diff options
| author | Max Resnick <max@ofmax.li> | 2023-04-02 08:52:52 -0700 |
|---|---|---|
| committer | Max Resnick <max@ofmax.li> | 2023-05-26 09:47:59 -0700 |
| commit | 480ce38f8c01de00adf6de651b8c2c57cd2148be (patch) | |
| tree | 85bd66986cb3f0ad501549e46e649073775e8077 /internal | |
| parent | 9e04be2ca3d8980ebc8ec791d005ba77382fb1fa (diff) | |
| download | go-git-server-480ce38f8c01de00adf6de651b8c2c57cd2148be.tar.gz | |
Adds a lot of tests adds middleware hook
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/middleware.go | 15 | ||||
| -rw-r--r-- | internal/admin/model.go | 102 | ||||
| -rw-r--r-- | internal/admin/model_test.go | 164 | ||||
| -rw-r--r-- | internal/admin/service.go | 56 | ||||
| -rw-r--r-- | internal/admin/service_test.go | 132 | ||||
| -rw-r--r-- | internal/authz/middleware.go | 2 | ||||
| -rw-r--r-- | internal/authz/middleware_test.go | 15 |
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()) |