aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorMax Resnick <max@ofmax.li>2022-11-25 21:28:54 -0800
committerMax Resnick <max@ofmax.li>2022-12-11 21:13:05 -0800
commitab38860d69c194969bea9ae5ef385c35eb94b988 (patch)
treec319053354cb2a772609b706368b1fd5741a44fc /internal
parent9e79e588131b0d59abefd84405cb7908bc2baa77 (diff)
downloadgo-git-server-ab38860d69c194969bea9ae5ef385c35eb94b988.tar.gz
add tests, new policies, init repo manager
Diffstat (limited to 'internal')
-rw-r--r--internal/admin/model.go194
-rw-r--r--internal/admin/model_test.go69
-rw-r--r--internal/admin/service.go37
-rw-r--r--internal/authz/middleware.go8
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)