diff options
| author | Max Resnick <max@ofmax.li> | 2022-11-25 21:28:54 -0800 |
|---|---|---|
| committer | Max Resnick <max@ofmax.li> | 2022-12-11 21:13:05 -0800 |
| commit | ab38860d69c194969bea9ae5ef385c35eb94b988 (patch) | |
| tree | c319053354cb2a772609b706368b1fd5741a44fc /internal | |
| parent | 9e79e588131b0d59abefd84405cb7908bc2baa77 (diff) | |
| download | go-git-server-ab38860d69c194969bea9ae5ef385c35eb94b988.tar.gz | |
add tests, new policies, init repo manager
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/admin/model.go | 194 | ||||
| -rw-r--r-- | internal/admin/model_test.go | 69 | ||||
| -rw-r--r-- | internal/admin/service.go | 37 | ||||
| -rw-r--r-- | internal/authz/middleware.go | 8 |
4 files changed, 304 insertions, 4 deletions
diff --git a/internal/admin/model.go b/internal/admin/model.go new file mode 100644 index 0000000..59f2498 --- /dev/null +++ b/internal/admin/model.go @@ -0,0 +1,194 @@ +// Package admin manage repos +package admin + +import ( + "errors" + "fmt" + "io" + "io/fs" + "log" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" + + "gopkg.in/ini.v1" +) + +const ( + // Read mode operations for repo + Read Action = 0 + // Write mode operations for repo + Write = 1 + // Admin mode operations for repo + Admin = 2 + // GitExportMagic magic file name for daemon export + GitExportMagic = "git-daemon-export-ok" +) + +// Action composite type for modes +type Action int + +// GitWeb git web configuration +type GitWeb struct { + Owner string `json:"owner"` + Description string `json:"description"` + Category string `json:"category"` + URL string `json:"url"` +} + +// Permission authorization controls +type Permission struct { + Role string `json:"role"` + Mode Action `json:"mode"` +} + +// GitRepo git repository +type GitRepo struct { + // Public "git-daemon-export-ok" magic file for git-http-backend + Public bool `json:"public"` + // Name game of repository + Name string `json:"name"` + // Web config settings + GitWebConfig *GitWeb `json:"git_web_config"` + // Permissions for authorization + Permissions []*Permission `json:"permissions"` +} + +// ServerRepos repos that are part of this server instance +type ServerRepos struct { + Name string `json:"name"` + Version string `json:"version"` + Repos []*GitRepo `json:"repos"` + BasePath string `json:"basepath"` +} + +func loadServerConfig(configPath string) *ServerRepos { + file, err := os.Open(configPath) + if err != nil { + log.Fatalf("Failed to open gitserver config %s", err) + } + defer file.Close() + b, err := io.ReadAll(file) + if err != nil { + log.Fatalf("Failed to read the gitserver config %s", err) + } + config := &ServerRepos{} + err = yaml.Unmarshal(b, &config) + if err != nil { + log.Fatalf("Failed to parse gitserver config %s", err) + } + return config +} + +// ServerPolicies generate casbin policies +func (s *ServerRepos) ServerPolicies() [][]string { + policies := [][]string{} + for _, repo := range s.Repos { + policies = append(policies, repo.CasbinPolicies()...) + } + return policies +} + +// ConfigureRepos run reconciler for all repos +func (s *ServerRepos) ConfigureRepos() { + for _, repo := range s.Repos { + repo.ReconcileRepo(s.BasePath) + } +} + +func readOnlyPaths(role, repoName string) [][]string { + return [][]string{ + []string{role, fmt.Sprintf("/%s/info/refs", repoName), "GET"}, + []string{role, fmt.Sprintf("/%s/git-upload-pack", repoName), "POST"}, + } +} +func writePaths(role, repoName string) [][]string { + return [][]string{[]string{role, fmt.Sprintf("/%s/git-recieve-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 + if p.Mode >= Read { + policies = append(policies, readOnlyPaths(p.Role, repoName)...) + } + // if write mode + if p.Mode >= Write { + policies = append(policies, writePaths(p.Role, repoName)...) + } + return policies +} + +// CasbinPolicies generate all policies +func (r *GitRepo) CasbinPolicies() [][]string { + policies := [][]string{} + for _, perm := range r.Permissions { + policies = append(policies, perm.Policy(r.Name)...) + } + return policies +} + +// ReconcileRepo update repo export settings, update web config +func (r *GitRepo) ReconcileRepo(basePath string) { + // if exist -> continue + repoBase := filepath.Join(basePath, r.Name, ".git") + _, err := os.Stat(repoBase) + if errors.Is(err, fs.ErrNotExist) { + // if no exist -> init bare + } + r.ConfigureExport(repoBase) + + if r.GitWebConfig == nil { + r.GitWebConfig = &GitWeb{} + } + r.GitWebConfig.ReconcileGitConf(repoBase) +} + +// ConfigureExport setup repo for sharing and configure web settings +func (r *GitRepo) ConfigureExport(basePath string) { + // do nothing on public repos + repoBase := fmt.Sprintf("%s.git", r.Name) + okExport := filepath.Join(repoBase, r.Name, 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() + if err != nil { + log.Fatalf("git-daemon-export-ok coudln't be created %s", err) + return + } +} + +// ReconcileGitConf reconcile gitweb configuration section of gitconfig +func (r *GitWeb) ReconcileGitConf(repoBase string) { + confPath := filepath.Join(repoBase, "conf") + cfg, err := ini.Load(confPath) + if err != nil { + log.Fatal("Coudln't read gitconfig") + } + // check if empty, delete + if (GitWeb{} == *r) { + if cfg.HasSection("gitweb") { + cfg.DeleteSection("gitweb") + } + return + } + section := cfg.Section("gitweb") + section.Key("description").SetValue(r.Description) + section.Key("owner").SetValue(r.Owner) + section.Key("url").SetValue(r.URL) + section.Key("category").SetValue(r.Category) + cfg.SaveTo(confPath) +} diff --git a/internal/admin/model_test.go b/internal/admin/model_test.go new file mode 100644 index 0000000..b13bfad --- /dev/null +++ b/internal/admin/model_test.go @@ -0,0 +1,69 @@ +package admin + +import ( + "fmt" + "testing" +) + +func TestCasbinPolicies(t *testing.T) { + roleName := "mr:role" + repoName := "myrepo" + pRO := &Permission{ + Role: roleName, + Mode: 0, + } + pW := &Permission{ + Role: "my:admin", + Mode: 1, + } + + t.Run("test read only policies", func(t *testing.T) { + roPolicies := readOnlyPaths(roleName, repoName) + for _, v := range roPolicies { + if v[0] != roleName { + t.Fatalf("Missing rolename in policy %s %s", v[0], v[1]) + } + } + if roPolicies[0][1] != fmt.Sprintf("/%s/info/refs", repoName) { + t.Fatal("missing info/refs policy") + } + if roPolicies[1][1] != fmt.Sprintf("/%s/git-upload-pack", repoName) { + t.Fatal("missing git-upload-pack policy") + } + if roPolicies[0][2] != "GET" { + t.Fatal("missing info/refs policy") + } + if roPolicies[1][2] != "POST" { + t.Fatal("missing git-upload-pack policy") + } + }) + t.Run("testing write policies", func(t *testing.T) { + wPolicies := writePaths(roleName, repoName) + if wPolicies[0][0] != roleName { + t.Fatal("Role name doesn't match") + } + if wPolicies[0][1] != fmt.Sprintf("/%s/git-recieve-pack", repoName) { + t.Fatal("Policy missing write path") + } + }) + + t.Run("testing mode build policies", func(t *testing.T) { + rOPolicy := pRO.Policy(roleName) + wPolicy := pW.Policy(roleName) + if len(rOPolicy) != 2 { + t.Fatal("Didn't provide correct number of read policies") + } + if len(wPolicy) != 3 { + t.Fatal("Didn't provide correct number of write policies") + } + }) + t.Run("testing repo level policies", func(t *testing.T) { + repo := &GitRepo{ + Permissions: []*Permission{pRO, pW}, + } + policies := repo.CasbinPolicies() + if len(policies) != 5 { + t.Fatal("Repo was expected to have 5 policies generated") + } + }) +} diff --git a/internal/admin/service.go b/internal/admin/service.go new file mode 100644 index 0000000..c09ad66 --- /dev/null +++ b/internal/admin/service.go @@ -0,0 +1,37 @@ +package admin + +import ( + "log" + + casbin "github.com/casbin/casbin/v2" +) + +// Servicer container for dependencies and functions +type Servicer struct { + *casbin.SyncedEnforcer + Conf *ServerRepos +} + +// InitServer initialize a git server and configure +func (s *Servicer) InitServer() { + policies := s.Conf.ServerPolicies() + s.AddPolicies(policies) + s.SavePolicy() + s.LoadPolicy() + s.Conf.ServerPolicies() +} + +// NewService create a new admin service, load config, and generate policies +func NewService(modelPath, policyPath, serverConfigPath string) *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) + svc := &Servicer{ + enf, + conf, + } + svc.InitServer() + return svc +} diff --git a/internal/authz/middleware.go b/internal/authz/middleware.go index 1dd06e3..41672f2 100644 --- a/internal/authz/middleware.go +++ b/internal/authz/middleware.go @@ -7,11 +7,11 @@ import ( "log" "net/http" - "github.com/casbin/casbin/v2" + "git.ofmax.li/go-git-server/internal/admin" "golang.org/x/crypto/bcrypt" ) -func Authentication(authMap TokenMap, next http.HandlerFunc) http.Handler { +func Authentication(authMap TokenMap, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { u, p, ok := req.BasicAuth() if !ok { @@ -40,13 +40,13 @@ func Authentication(authMap TokenMap, next http.HandlerFunc) http.Handler { } // Authorization middleware to enforce authoirzation of all requests. -func Authorization(enf *casbin.Enforcer, next http.HandlerFunc) http.Handler { +func Authorization(adminSvc *admin.Service, next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() urn := ctx.Value("urn") repo := req.URL.Path action := req.Method - ok, err := enf.Enforce(urn, repo, action) + ok, err := adminSvc.Enforce(urn, repo, action) if err != nil { log.Printf("error running enforce %s", err) http.Error(rw, "Bad Request", http.StatusBadRequest) |