// Package admin manage repos package admin import ( "errors" "fmt" "io" "io/fs" "log" "os" "path/filepath" "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" "sigs.k8s.io/yaml" ) 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, fmt.Sprintf("%s.git", r.Name)) _, err := os.Stat(repoBase) if errors.Is(err, fs.ErrNotExist) { // if no exist -> init bare fs := osfs.New(repoBase) strg := filesystem.NewStorage(fs, nil) _, _ = git.Init(strg, nil) } 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(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() if err != nil { log.Fatalf("git-daemon-export-ok coudln't be created %s", err) } } // ReconcileGitConf reconcile gitweb configuration section of gitconfig func (r *GitWeb) ReconcileGitConf(repoBase string) { confPath := filepath.Join(repoBase, "config") cfg, err := ini.Load(confPath) if err != nil { log.Fatalf("Coudln't read gitconfig %s", err) } // check if empty, delete if (GitWeb{} == *r) { if cfg.HasSection("gitweb") { cfg.DeleteSection("gitweb") cfg.SaveTo(confPath) } 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) }