aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorMax Resnick <max@ofmax.li>2023-04-02 08:52:52 -0700
committerMax Resnick <max@ofmax.li>2023-05-26 09:47:59 -0700
commit480ce38f8c01de00adf6de651b8c2c57cd2148be (patch)
tree85bd66986cb3f0ad501549e46e649073775e8077 /internal
parent9e04be2ca3d8980ebc8ec791d005ba77382fb1fa (diff)
downloadgo-git-server-480ce38f8c01de00adf6de651b8c2c57cd2148be.tar.gz
Adds a lot of tests adds middleware hook
Diffstat (limited to 'internal')
-rw-r--r--internal/admin/middleware.go15
-rw-r--r--internal/admin/model.go102
-rw-r--r--internal/admin/model_test.go164
-rw-r--r--internal/admin/service.go56
-rw-r--r--internal/admin/service_test.go132
-rw-r--r--internal/authz/middleware.go2
-rw-r--r--internal/authz/middleware_test.go15
7 files changed, 411 insertions, 75 deletions
diff --git a/internal/admin/middleware.go b/internal/admin/middleware.go
new file mode 100644
index 0000000..56d4797
--- /dev/null
+++ b/internal/admin/middleware.go
@@ -0,0 +1,15 @@
+package admin
+
+import (
+ "log"
+ "net/http"
+)
+
+// Admin middleware to handle requests to the admin repo.
+func AdminHooks(adminSvc *Servicer, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+ log.Printf("stuffs about to reload %s", "now")
+ next.ServeHTTP(rw, req)
+ go adminSvc.Reload()
+ })
+}
diff --git a/internal/admin/model.go b/internal/admin/model.go
index cf69fcd..5a7f984 100644
--- a/internal/admin/model.go
+++ b/internal/admin/model.go
@@ -10,9 +10,11 @@ import (
"os"
"path/filepath"
+ "github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/storage/filesystem"
+ "github.com/go-git/go-git/v5/storage/memory"
"gopkg.in/ini.v1"
@@ -28,6 +30,8 @@ const (
Admin = 2
// GitExportMagic magic file name for daemon export
GitExportMagic = "git-daemon-export-ok"
+ // GitWebExportMagic
+ GitWebExportMagic = "git-web-export-ok"
)
// Action composite type for modes
@@ -67,22 +71,65 @@ type ServerRepos struct {
BasePath string `json:"basepath"`
}
-func loadServerConfig(configPath string) *ServerRepos {
- file, err := os.Open(configPath)
+func loadFromGit(gitUrl, filePath string) ([]byte, error) {
+ fs := memfs.New()
+ storer := memory.NewStorage()
+ _, err := git.Clone(storer, fs, &git.CloneOptions{
+ URL: gitUrl,
+ })
if err != nil {
- log.Fatalf("Failed to open gitserver config %s", err)
+ // log.error
+ fmt.Printf("coudln't clone mgmt repo %s", err)
+ return []byte(""), errors.New("coudln't clone mgmt repo")
+ }
+ file, err := fs.Open(filePath)
+ if err != nil {
+ fmt.Printf("Failed to open gitserver config %s", err)
+ return []byte(""), errors.New("coudln't open git config file from mgmt repo")
}
defer file.Close()
- b, err := io.ReadAll(file)
+ return io.ReadAll(file)
+}
+
+func loadLocalFile(path string) ([]byte, error) {
+ file, err := os.Open(path)
+ if err != nil {
+ log.Printf("config file not opened %s", path)
+ return []byte{}, err
+ }
+ configBytes, err := io.ReadAll(file)
if err != nil {
- log.Fatalf("Failed to read the gitserver config %s", err)
+ log.Print("config file not read")
+ return []byte{}, err
+ }
+ return configBytes, nil
+}
+
+func loadServerConfig(mgmtRepo bool, baseDir, configPath string) (*ServerRepos, error) {
+ configBytes := []byte{}
+ var err error
+ if mgmtRepo {
+ repoURI := filepath.Join("file:///", baseDir, "mgmt.git")
+ configBytes, err = loadFromGit(repoURI, configPath)
+ if err != nil {
+ // log.error
+ log.Print("Failed to load config file from git")
+ return &ServerRepos{}, err
+ }
+ } else {
+ configBytes, err = loadLocalFile(filepath.Join(baseDir, configPath))
+ if err != nil {
+ // log.error
+ log.Print("Failed to load config file from git")
+ return &ServerRepos{}, err
+ }
}
config := &ServerRepos{}
- err = yaml.Unmarshal(b, &config)
+ err = yaml.Unmarshal(configBytes, &config)
if err != nil {
- log.Fatalf("Failed to parse gitserver config %s", err)
+ return &ServerRepos{}, errors.New("Could not parse gitserver config")
}
- return config
+ return config, nil
}
// ServerPolicies generate casbin policies
@@ -108,19 +155,20 @@ func readOnlyPaths(role, repoName string) [][]string {
}
}
func writePaths(role, repoName string) [][]string {
- return [][]string{[]string{role, fmt.Sprintf("/%s/git-recieve-pack", repoName), "POST"}}
+ return [][]string{[]string{role, fmt.Sprintf("/%s/git-receive-pack", repoName), "POST"}}
}
// Policy generate policy for repo base on mode
func (p *Permission) Policy(repoName string) [][]string {
policies := [][]string{}
// if read mode or greater e.g. write mode
+ roleName := fmt.Sprintf("role:%s", p.Role)
if p.Mode >= Read {
- policies = append(policies, readOnlyPaths(p.Role, repoName)...)
+ policies = append(policies, readOnlyPaths(roleName, repoName)...)
}
// if write mode
if p.Mode >= Write {
- policies = append(policies, writePaths(p.Role, repoName)...)
+ policies = append(policies, writePaths(roleName, repoName)...)
}
return policies
}
@@ -145,8 +193,18 @@ func (r *GitRepo) ReconcileRepo(basePath string) {
strg := filesystem.NewStorage(fs, nil)
_, _ = git.Init(strg, nil)
}
+ // set export file for git-http-backend
+ okExport := filepath.Join(repoBase, GitExportMagic)
+ _, err = os.Stat(okExport)
+ if errors.Is(err, fs.ErrNotExist) {
+ // Create web export
+ f, err := os.Create(okExport)
+ f.Close()
+ if err != nil {
+ log.Fatalf("%s coudln't be created %s", GitExportMagic, err)
+ }
+ }
r.ConfigureExport(repoBase)
-
if r.GitWebConfig == nil {
r.GitWebConfig = &GitWeb{}
}
@@ -155,24 +213,10 @@ func (r *GitRepo) ReconcileRepo(basePath string) {
// ConfigureExport setup repo for sharing and configure web settings
func (r *GitRepo) ConfigureExport(repoBase string) {
- // do nothing on public repos
- okExport := filepath.Join(repoBase, GitExportMagic)
- _, err := os.Stat(okExport)
- // Not public but the export setting is setting exists
- if !r.Public && err == nil {
- // delete file
- os.Remove(okExport)
- return
- }
- // Not public and the file doesn't exist
- if !r.Public && errors.Is(err, fs.ErrNotExist) {
- return
- }
- //
- f, err := os.Create(okExport)
- defer f.Close()
+ okExport := filepath.Join(repoBase, GitWebExportMagic)
+ _, err := os.Create(okExport)
if err != nil {
- log.Fatalf("git-daemon-export-ok coudln't be created %s", err)
+ log.Fatalf("%s coudln't be created %s", GitWebExportMagic, err)
}
}
diff --git a/internal/admin/model_test.go b/internal/admin/model_test.go
index 79e3cb5..7f816f5 100644
--- a/internal/admin/model_test.go
+++ b/internal/admin/model_test.go
@@ -1,8 +1,10 @@
package admin
import (
+ "bytes"
"errors"
"fmt"
+ "io"
"io/fs"
"io/ioutil"
"os"
@@ -10,18 +12,21 @@ import (
"strings"
"testing"
+ "github.com/go-git/go-billy/v5/osfs"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/storage/filesystem"
"gopkg.in/ini.v1"
)
func TestCasbinPolicies(t *testing.T) {
- roleName := "mr:role"
+ roleName := "myrole"
repoName := "myrepo"
pRO := &Permission{
Role: roleName,
Mode: 0,
}
pW := &Permission{
- Role: "my:admin",
+ Role: "admin",
Mode: 1,
}
@@ -50,7 +55,7 @@ func TestCasbinPolicies(t *testing.T) {
if wPolicies[0][0] != roleName {
t.Fatal("Role name doesn't match")
}
- if wPolicies[0][1] != fmt.Sprintf("/%s/git-recieve-pack", repoName) {
+ if wPolicies[0][1] != fmt.Sprintf("/%s/git-receive-pack", repoName) {
t.Fatal("Policy missing write path")
}
})
@@ -76,6 +81,128 @@ func TestCasbinPolicies(t *testing.T) {
})
}
+func TestLoadServerConfig(t *testing.T) {
+ t.Run("testing server config from file", func(t *testing.T) {
+ localDir := t.TempDir()
+ // TODO Refactor next touch
+ localFile := filepath.Join(localDir, "stuff.yaml")
+ srcFile, err := os.Open("../../gitserver.yaml")
+ if err != nil {
+ t.Fatalf("Error opening base config %s", err)
+ }
+ defer srcFile.Close()
+
+ // dest
+ destFile, err := os.OpenFile(localFile, os.O_RDWR|os.O_CREATE, 0755)
+ if err != nil {
+ t.Fatalf("failed to open destination in git repo %s", err)
+ }
+ defer destFile.Close()
+
+ // copy
+ if _, err := io.Copy(destFile, srcFile); err != nil {
+ t.Fatalf("Error copying file %s", err)
+ }
+
+ // end copy file
+ loadedFile, err := loadServerConfig(false, localDir, "stuff.yaml")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(loadedFile.Repos) != 2 {
+ t.Fatalf("expected to find 2 repos found %d", len(loadedFile.Repos))
+ }
+ })
+
+ t.Run("testing server config from git", func(t *testing.T) {
+
+ })
+}
+
+func TestLocalFile(t *testing.T) {
+ localDir := t.TempDir()
+ localFile := filepath.Join(localDir, "stuff.yaml")
+ os.WriteFile(localFile, []byte("stuff"), 0750)
+ loadedFile, err := loadLocalFile(localFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if !bytes.Contains(loadedFile, []byte("stuff")) {
+ t.Fatal("failed to find expected contents in localfile")
+ }
+ _, err = loadLocalFile("dne.txt")
+ if err == nil {
+ t.Fatal("Expected to find and error and didn't")
+ }
+}
+
+func TestMgmtGitConfig(t *testing.T) {
+ // setup tempdir
+ gitDir := t.TempDir()
+ // init git repo
+ gitFs := osfs.New(gitDir)
+ strg := filesystem.NewStorage(gitFs, nil)
+ repo, _ := git.Init(strg, gitFs)
+ // add file
+
+ // src
+ srcFile, err := os.Open("../../gitserver.yaml")
+ if err != nil {
+ t.Fatalf("Error opening base config %s", err)
+ }
+ defer srcFile.Close()
+
+ // file name
+ // fileToCommit := fs.Join(gitDir, "gitserver.yaml")
+ fileToCommit := "gitserver.yaml"
+
+ // dest
+ destFile, err := gitFs.OpenFile(fileToCommit, os.O_RDWR|os.O_CREATE, 0755)
+ if err != nil {
+ t.Fatalf("failed to open destination in git repo %s", err)
+ }
+ defer destFile.Close()
+
+ // copy
+ if _, err := io.Copy(destFile, srcFile); err != nil {
+ t.Fatalf("Error copying file %s", err)
+ }
+ // commit
+ wt, err := repo.Worktree()
+ if err != nil {
+ t.Fatal(err)
+ }
+ wt.Add(fileToCommit)
+ _, err = wt.Commit(fileToCommit, &git.CommitOptions{})
+ if err != nil {
+ t.Fatalf("Error creating commit %s", err)
+ }
+
+ // run load func
+ content, err := loadFromGit(gitDir, "gitserver.yaml")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // "go-git-server"
+ if !bytes.Contains(content, []byte("go-git-server")) {
+ t.Fatal("config missing expected")
+ }
+
+ // check couldnt clone err
+ _, err = loadFromGit("/dne/bar", "gitserver.yaml")
+ if err == nil {
+ t.Fatal("expected an cloning repo didn't find one")
+ }
+ // check couldnt open file err
+ _, err = loadFromGit(gitDir, "dne.yaml")
+ if err == nil {
+ t.Fatal("expected an error opening config file didn't find one")
+ }
+ // TODO run via serverLoadConfig
+}
+
func TestConfigReconcile(t *testing.T) {
tempDir := t.TempDir()
defer os.RemoveAll(tempDir)
@@ -89,8 +216,7 @@ func TestConfigReconcile(t *testing.T) {
}
f.Close()
repo := &GitRepo{
- Public: true,
- Name: "testrepo",
+ Name: "testrepo",
}
t.Run("test add gitweb section and remove it", func(t *testing.T) {
// make "fake" repo
@@ -126,7 +252,7 @@ func TestConfigReconcile(t *testing.T) {
}
})
t.Run("test magic export file is created", func(t *testing.T) {
- exportPath := filepath.Join(testRepo, GitExportMagic)
+ exportPath := filepath.Join(testRepo, GitWebExportMagic)
repo.ConfigureExport(testRepo)
_, err := os.Stat(exportPath)
if errors.Is(err, fs.ErrNotExist) {
@@ -135,13 +261,6 @@ func TestConfigReconcile(t *testing.T) {
if err != nil {
t.Fatalf("encountered an error %s", err)
}
- // copy repo
- pvtRepo := repo
- pvtRepo.Public = false
- pvtRepo.ConfigureExport(testRepo)
- if _, err := os.Stat(exportPath); err == nil {
- t.Fatal("expected export file exist, but does not exist")
- }
})
}
@@ -150,8 +269,7 @@ func TestRepoReconcile(t *testing.T) {
print(tempDir)
// defer os.RemoveAll(tempDir)
repo := &GitRepo{
- Public: true,
- Name: "testrepo",
+ Name: "TestMeRepo",
GitWebConfig: &GitWeb{
"owner",
"description",
@@ -181,23 +299,9 @@ bare = true
if !strings.Contains(string(content), "description") {
t.Fatal("expected to find 'description' in config, didn't found", string(content))
}
- gitExportMagicPath := filepath.Join(tempDir, fmt.Sprintf("%s.git", repo.Name), GitExportMagic)
+ gitExportMagicPath := filepath.Join(tempDir, fmt.Sprintf("%s.git", repo.Name), GitWebExportMagic)
if _, err := os.Stat(gitExportMagicPath); errors.Is(err, fs.ErrNotExist) {
t.Fatal("expected git export magic to be created, but does not exist")
}
- // Test that repo is switched back to private
- repo.Public = false
- // re-write the base config to repo
- ioutil.WriteFile(tempConfigFile, defaultFile, 0644)
- // re-reconcile
- repo.ReconcileRepo(tempDir)
- // check if description is *NOT* in the file
- if !strings.Contains(string(content), "description") {
- t.Fatal("expected to *NOT* find 'description' in config, didn't found", string(content))
- }
- // make sure export is removed
- if _, err := os.Stat(gitExportMagicPath); !errors.Is(err, fs.ErrNotExist) {
- t.Fatal("expected git export magic to not exist, but *does* exist")
- }
}
diff --git a/internal/admin/service.go b/internal/admin/service.go
index 80056b7..84547fa 100644
--- a/internal/admin/service.go
+++ b/internal/admin/service.go
@@ -9,28 +9,72 @@ import (
// Servicer container for dependencies and functions
type Servicer struct {
*casbin.SyncedEnforcer
- Conf *ServerRepos
+ Conf *ServerRepos
+ serverConfigPath string
+ reposDir string
+ mgmtRepo bool
+}
+
+// Reload reoload server config and sync policies
+func (s *Servicer) Reload() {
+ tmpConfig, err := loadServerConfig(s.mgmtRepo, s.reposDir, s.serverConfigPath)
+ if err != nil {
+ // log.error
+ log.Printf("failed to load config %s", err)
+ log.Print("refusing to reload config")
+ return
+ }
+ s.Conf = tmpConfig
+ s.InitServer()
}
// InitServer initialize a git server and configure
func (s *Servicer) InitServer() {
policies := s.Conf.ServerPolicies()
- s.AddPolicies(policies)
- s.SavePolicy()
- s.LoadPolicy()
+ log.Print("policies generated")
+ numAdded := 0
+ for _, policy := range policies {
+ added, err := s.AddPolicy(policy[0], policy[1], policy[2])
+ if err != nil {
+ // log.error
+ log.Printf("error adding policy %s %s %s error %s", policy[0], policy[1], policy[2], err)
+ continue
+ }
+ if added {
+ numAdded += 1
+ }
+ }
+ log.Printf("policies added %d", numAdded)
+ if err := s.SavePolicy(); err != nil {
+ log.Print("couldn't save policy")
+ }
+ log.Printf("policies saved")
+ if err := s.LoadPolicy(); err != nil {
+ log.Print("cloudn't load policy")
+ }
+ log.Print("policies loaded")
s.Conf.ConfigureRepos()
+ log.Print("configured repos")
}
// NewService create a new admin service, load config, and generate policies
-func NewService(modelPath, policyPath, serverConfigPath string) *Servicer {
+func NewService(modelPath, policyPath, serverConfigPath, reposDir string, mgmtRepo bool) *Servicer {
enf, err := casbin.NewSyncedEnforcer(modelPath, policyPath)
if err != nil {
log.Fatalf("Couldn't load the enforcer encountered the following error: %s", err)
}
- conf := loadServerConfig(serverConfigPath)
+
+ conf, err := loadServerConfig(mgmtRepo, reposDir, serverConfigPath)
+ if err != nil {
+ // log.error
+ log.Fatalf("Coudln't load server config %s", err)
+ }
svc := &Servicer{
enf,
conf,
+ serverConfigPath,
+ reposDir,
+ mgmtRepo,
}
svc.InitServer()
return svc
diff --git a/internal/admin/service_test.go b/internal/admin/service_test.go
new file mode 100644
index 0000000..fdd3aa6
--- /dev/null
+++ b/internal/admin/service_test.go
@@ -0,0 +1,132 @@
+package admin
+
+import (
+ "io"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+var (
+ updatedServerConfig []byte = []byte(`
+---
+name: "go-git-server"
+version: "v1alpha1"
+basepath: ./repos
+repos:
+ - name: mgmt
+ permissions:
+ - role: admin
+ mode: 1
+ - name: testmerepo
+ git_web_config:
+ owner: grumps
+ description: >-
+ A wrapper to git http-backend providing authentcation and authorization
+ inspired by gitolite.
+ permissions:
+ - role: maintainers
+ mode: 1
+ - name: thisismynewrepo
+ git_web_config:
+ owner: grumps
+ description: >-
+ A wrapper to git http-backend providing authentcation and authorization
+ inspired by gitolite.
+ permissions:
+ - role: maintainers
+ mode: 1
+`)
+)
+
+func copyFile(t *testing.T, srcFilePath, destPath string) {
+
+ srcFile, err := os.Open(srcFilePath)
+ if err != nil {
+ t.Fatalf("Error opening base config %s", err)
+ }
+ defer srcFile.Close()
+
+ // dest
+ destFile, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE, 0755)
+ if err != nil {
+ t.Fatalf("failed to open destination in git repo %s", err)
+ }
+ defer destFile.Close()
+
+ // copy
+ if _, err := io.Copy(destFile, srcFile); err != nil {
+ t.Fatalf("Error copying file %s", err)
+ }
+}
+
+func TestInitServer(t *testing.T) {
+ tempDir := t.TempDir()
+ tempRepoDir := t.TempDir()
+
+ // auth model
+ destModelFile := filepath.Join(tempDir, "auth_model.ini")
+ srcModelFile := "../../auth_model.ini"
+ copyFile(t, srcModelFile, destModelFile)
+ // end auth model
+
+ // policy
+ destPolicyFile := filepath.Join(tempDir, "testpolicy.csv")
+ srcPolicyFile := "../../testpolicy.csv"
+ copyFile(t, srcPolicyFile, destPolicyFile)
+ // end policy
+
+ // config
+ destConfigFile := filepath.Join(tempRepoDir, "gitserver.yaml")
+ srcConfigFile := "../../gitserver.yaml"
+ copyFile(t, srcConfigFile, destConfigFile)
+ // end config
+
+ t.Run("test reload config success", func(t *testing.T) {
+ svc := NewService(destModelFile,
+ destPolicyFile,
+ "gitserver.yaml",
+ tempRepoDir,
+ false)
+ err := os.WriteFile(destConfigFile, updatedServerConfig, 0755)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // stuff
+ svc.Reload()
+ // check policy file to make sure it was saved
+ data, err := os.ReadFile(destPolicyFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(data), "thisismynewrepo") {
+ t.Fatal("expected to find test new repo but didn't")
+ }
+
+ })
+ t.Run("test reload config err", func(t *testing.T) {
+ svc := NewService(destModelFile,
+ destPolicyFile,
+ "gitserver.yaml",
+ tempRepoDir,
+ false)
+ notAGoodConfig := []byte("this is not valid yaml")
+ err := os.WriteFile(destConfigFile, notAGoodConfig, 0755)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // stuff
+ svc.Reload()
+ // check policy file to make sure it wasn't saved
+ data, err := os.ReadFile(destPolicyFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if !strings.Contains(string(data), "mgmt") {
+ log.Fatal("expected to mgmt repo but didn't in policy")
+ }
+
+ })
+}
diff --git a/internal/authz/middleware.go b/internal/authz/middleware.go
index f01f262..a35b6b4 100644
--- a/internal/authz/middleware.go
+++ b/internal/authz/middleware.go
@@ -43,7 +43,7 @@ func Authentication(authMap TokenMap, next http.Handler) http.Handler {
func Authorization(adminSvc *admin.Servicer, next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ctx := req.Context()
- urn := ctx.Value("urn")
+ urn := ctx.Value("urn").(string)
repo := req.URL.Path
action := req.Method
ok, err := adminSvc.Enforce(urn, repo, action)
diff --git a/internal/authz/middleware_test.go b/internal/authz/middleware_test.go
index 5795b3f..cc3f6d1 100644
--- a/internal/authz/middleware_test.go
+++ b/internal/authz/middleware_test.go
@@ -8,7 +8,6 @@ import (
"testing"
"git.ofmax.li/go-git-server/internal/admin"
- "github.com/casbin/casbin/v2"
)
func junkTestHandler() http.HandlerFunc {
@@ -85,10 +84,6 @@ func TestAuthentication(t *testing.T) {
func TestAuthorization(t *testing.T) {
t.Log("Starting authorization tests")
baseURL := "http://test"
- enf, err := casbin.NewSyncedEnforcer("../../auth_model.ini", "../../testpolicy.csv")
- if err != nil {
- t.Fatalf("Failed to load policies\n%s", err)
- }
cases := []struct {
url string
user string
@@ -108,10 +103,12 @@ func TestAuthorization(t *testing.T) {
description: "an unautorized action should yield a 403",
},
}
- svcr := &admin.Servicer{
- enf,
- &admin.ServerRepos{},
- }
+ svcr := admin.NewService(
+ "../../auth_model.ini",
+ "../../testpolicy.csv",
+ "../../gitserver.yaml",
+ "../../repos",
+ false)
for _, tc := range cases {
t.Logf("test case: %s", tc.description)
authHandler := Authorization(svcr, junkTestHandler())