aboutsummaryrefslogtreecommitdiff
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
parent9e79e588131b0d59abefd84405cb7908bc2baa77 (diff)
downloadgo-git-server-ab38860d69c194969bea9ae5ef385c35eb94b988.tar.gz
add tests, new policies, init repo manager
-rw-r--r--README.md25
-rw-r--r--cmd/main.go20
-rw-r--r--config.ini7
-rw-r--r--gitserver.yaml15
-rw-r--r--go.mod3
-rw-r--r--go.sum8
-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
-rw-r--r--policy.csv19
11 files changed, 382 insertions, 23 deletions
diff --git a/README.md b/README.md
index 0b04925..75e2f0c 100644
--- a/README.md
+++ b/README.md
@@ -26,3 +26,28 @@ Tools like gitea are great, but they require things like a DBMS. This increases
* create repo
* update gitweb config per repo
* migration from gitolite
+
+## Admin
+
+
+### Admin events
+
+triggered by handler?
+triggered by hooks?
+
+* [ ] new repo
+* [ ] admin push
+
+### Git Mgmt
+
+* [ ] git web export
+* [ ] web description
+
+### Policy Mgmt
+
+* [x] policy generate
+* [x] upsert policies
+
+## Hooks
+
+what's the env for this?
diff --git a/cmd/main.go b/cmd/main.go
index 962eaa1..94c5d13 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -6,8 +6,7 @@ import (
"log"
"net/http"
- "github.com/casbin/casbin/v2"
-
+ "git.ofmax.li/go-git-server/internal/admin"
"git.ofmax.li/go-git-server/internal/authz"
"git.ofmax.li/go-git-server/internal/git"
)
@@ -16,16 +15,15 @@ var (
reposDir = flag.String("r", "./repos", "Directory containing git repositories")
backendCommand = flag.String("c", "git http-backend", "CGI binary to execute")
addr = flag.String("l", ":8080", "Address/port to listen on")
+ modelPath = flag.String("m", "./auth_model.ini", "Authentication model")
+ policyPath = flag.String("p", "./policy.csv", "auth policy")
+ serverConfig = flag.String("s", "./gitserver.yaml", "serverconfig path")
newToken = flag.Bool("t", false, "make a new token")
+ updatePolicies = flag.Bool("u", false, "update policies")
)
func main() {
- // TODO this should be configurable
- enf, err := casbin.NewEnforcer("./auth_model.ini", "./policy.csv")
- if err != nil {
- log.Fatalf("Couldn't load the enforcer encountered the following error: %s", err)
- }
- flag.Parse()
+ admin.ReconcileGitConf("./config.ini")
if *newToken {
token, hash, err := authz.GenerateNewToken()
if err != nil {
@@ -34,8 +32,10 @@ func main() {
fmt.Printf("token: %s\nhash: %s\n", token, hash)
return
}
+ adminSvc := admin.NewService(*modelPath, *policyPath, *serverConfig)
+ adminSvc.InitServer()
tokens := authz.NewTokenMap()
- err = tokens.LoadTokensFromFile("./tokens.csv")
+ err := tokens.LoadTokensFromFile("./tokens.csv")
if err != nil {
log.Fatal(err)
}
@@ -43,6 +43,6 @@ func main() {
// TODO we don't want to use a global
// de-reference args
router.Handle("/", git.GitHttpBackendHandler(*reposDir, *backendCommand))
- mux := authz.Authentication(tokens, authz.Authorization(enf, router))
+ mux := authz.Authentication(tokens, authz.Authorization(adminSvc, router))
log.Fatal(http.ListenAndServe(":8080", mux))
}
diff --git a/config.ini b/config.ini
new file mode 100644
index 0000000..330c0d7
--- /dev/null
+++ b/config.ini
@@ -0,0 +1,7 @@
+[core]
+repositoryformatversion = 0
+filemode = true
+bare = true
+
+[gitweb]
+app_mode = production
diff --git a/gitserver.yaml b/gitserver.yaml
new file mode 100644
index 0000000..1ffc59d
--- /dev/null
+++ b/gitserver.yaml
@@ -0,0 +1,15 @@
+---
+name: "go-git-server"
+version: "v1alpha1"
+basepath: ./repos
+repos:
+ - name: testmerepo
+ public: true
+ git_web_config:
+ owner: grumps
+ description: |-
+ A wrapper to git http-backend providing authentcation and authorization
+ Inspired by gitolite
+ permissions:
+ - role: maintainers
+ mode: 1
diff --git a/go.mod b/go.mod
index dc11129..80aa875 100644
--- a/go.mod
+++ b/go.mod
@@ -8,4 +8,7 @@ require (
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/casbin/casbin v1.9.1 // indirect
github.com/casbin/casbin/v2 v2.56.0 // indirect
+ github.com/subpop/go-ini v0.1.4 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index 31ea3fb..3bafeaa 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,9 @@ github.com/casbin/casbin v1.9.1/go.mod h1:z8uPsfBJGUsnkagrt3G8QvjgTKFMBJ32UP8HpZ
github.com/casbin/casbin/v2 v2.56.0 h1:4qM+hDfj+i9M6lBbguafWKE/8tJA+9vRY5+l0ZB5WTo=
github.com/casbin/casbin/v2 v2.56.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/subpop/go-ini v0.1.4 h1:+OVDOLyoQQCkk36v48bDcBscw2GCn9cesQc6PFLYdg8=
+github.com/subpop/go-ini v0.1.4/go.mod h1:q0fhdlbGE3dI9dHPgUntXh1ggwR+SpfXL/kogOefaBE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
@@ -13,3 +16,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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)
diff --git a/policy.csv b/policy.csv
index 0722f5e..68bd11c 100644
--- a/policy.csv
+++ b/policy.csv
@@ -1,9 +1,10 @@
-p,role:admin,config,admin
-p,role:maintainers,/cch/info/refs,GET
-p,role:maintainers,/cch/git-upload-pack,POST
-p,role:maintainers,*,write
-
-g,role:admin,role:maintainers
-
-g,admin,role:admin
-g,uid:grumps,role:maintainers
+p, role:admin, config, admin
+p, role:maintainers, /go-git-server/git-upload-pack, POST
+p, role:maintainers, /go-git-server/info/refs, GET
+p, maintainers, /go-git-server/info/refs, GET
+p, maintainers, /go-git-server/git-upload-pack, POST
+p, maintainers, /go-git-server/git-recieve-pack, POST
+g, role:admin, role:maintainers
+g, admin, role:admin
+g, uid:grumps, role:maintainers
+g, aid:argo, role:bots \ No newline at end of file