this repo has no description

update repoguard to work with rbac

Akshay a37a33f5 1f7e1fad

Changed files
+107 -20
cmd
knotserver
repoguard
knotserver
rbac
+7 -1
cmd/knotserver/main.go
··· 46 l.Error("failed to setup server", "error", err) 47 return 48 } 49 - 50 addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) 51 52 l.Info("starting main server", "address", addr) 53 l.Error("server error", "error", http.ListenAndServe(addr, mux)) 54 return 55 }
··· 46 l.Error("failed to setup server", "error", err) 47 return 48 } 49 addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) 50 51 + imux := knotserver.Internal(ctx, db, e) 52 + iaddr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.InternalPort) 53 + 54 + l.Info("starting internal server", "address", iaddr) 55 + go http.ListenAndServe(iaddr, imux) 56 + 57 l.Info("starting main server", "address", addr) 58 l.Error("server error", "error", http.ListenAndServe(addr, mux)) 59 + 60 return 61 }
+44 -15
cmd/repoguard/main.go
··· 5 "flag" 6 "fmt" 7 "log" 8 "os" 9 "os/exec" 10 "path" ··· 21 clientIP string 22 23 // Command line flags 24 - allowedUser = flag.String("user", "", "Allowed git user") 25 - baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories") 26 - logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file") 27 ) 28 29 func main() { ··· 40 } 41 } 42 43 - if *allowedUser == "" { 44 exitWithLog("access denied: no user specified") 45 } 46 47 sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND") 48 49 logEvent("Connection attempt", map[string]interface{}{ 50 - "user": *allowedUser, 51 "command": sshCommand, 52 "client": clientIP, 53 }) ··· 63 64 gitCommand := cmdParts[0] 65 66 - // example.com/repo 67 - handlePath := strings.Trim(cmdParts[1], "'") 68 - repoName := handleToDid(handlePath) 69 70 validCommands := map[string]bool{ 71 "git-receive-pack": true, ··· 76 exitWithLog("access denied: invalid git command") 77 } 78 79 - did := path.Dir(repoName) 80 if gitCommand != "git-upload-pack" { 81 - if !isAllowedUser(*allowedUser, did) { 82 exitWithLog("access denied: user not allowed") 83 } 84 } 85 86 - fullPath := filepath.Join(*baseDirFlag, repoName) 87 fullPath = filepath.Clean(fullPath) 88 89 logEvent("Processing command", map[string]interface{}{ 90 - "user": *allowedUser, 91 "command": gitCommand, 92 "repo": repoName, 93 "fullPath": fullPath, ··· 104 } 105 106 logEvent("Command completed", map[string]interface{}{ 107 - "user": *allowedUser, 108 "command": gitCommand, 109 "repo": repoName, 110 "success": true, 111 }) 112 } 113 114 func handleToDid(handlePath string) string { ··· 166 } 167 } 168 169 - func isAllowedUser(user, did string) bool { 170 - return user == did 171 }
··· 5 "flag" 6 "fmt" 7 "log" 8 + "net/http" 9 + "net/url" 10 "os" 11 "os/exec" 12 "path" ··· 23 clientIP string 24 25 // Command line flags 26 + incomingUser = flag.String("user", "", "Allowed git user") 27 + baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories") 28 + logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file") 29 + endpoint = flag.String("internal-api", "http://localhost:5555", "Internal API endpoint") 30 ) 31 32 func main() { ··· 43 } 44 } 45 46 + if *incomingUser == "" { 47 exitWithLog("access denied: no user specified") 48 } 49 50 sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND") 51 52 logEvent("Connection attempt", map[string]interface{}{ 53 + "user": *incomingUser, 54 "command": sshCommand, 55 "client": clientIP, 56 }) ··· 66 67 gitCommand := cmdParts[0] 68 69 + // did:foo/repo-name or 70 + // handle/repo-name 71 + components := filepath.SplitList(cmdParts[2]) 72 + if len(components) != 2 { 73 + exitWithLog("invalid repo format, needs <user>/<repo>") 74 + } 75 + 76 + didOrHandle := components[0] 77 + did := resolveToDid(didOrHandle) 78 + repoName := components[1] 79 + qualifiedRepoName := filepath.Join(did, repoName) 80 81 validCommands := map[string]bool{ 82 "git-receive-pack": true, ··· 87 exitWithLog("access denied: invalid git command") 88 } 89 90 if gitCommand != "git-upload-pack" { 91 + if !isPushPermitted(*incomingUser, qualifiedRepoName) { 92 exitWithLog("access denied: user not allowed") 93 } 94 } 95 96 + fullPath := filepath.Join(*baseDirFlag, qualifiedRepoName) 97 fullPath = filepath.Clean(fullPath) 98 99 logEvent("Processing command", map[string]interface{}{ 100 + "user": *incomingUser, 101 "command": gitCommand, 102 "repo": repoName, 103 "fullPath": fullPath, ··· 114 } 115 116 logEvent("Command completed", map[string]interface{}{ 117 + "user": *incomingUser, 118 "command": gitCommand, 119 "repo": repoName, 120 "success": true, 121 }) 122 + } 123 + 124 + func resolveToDid(didOrHandle string) string { 125 + ident, err := auth.ResolveIdent(context.Background(), didOrHandle) 126 + if err != nil { 127 + exitWithLog(fmt.Sprintf("error resolving handle: %v", err)) 128 + } 129 + 130 + // did:plc:foobarbaz/repo 131 + return ident.DID.String() 132 } 133 134 func handleToDid(handlePath string) string { ··· 186 } 187 } 188 189 + func isPushPermitted(user, qualifiedRepoName string) bool { 190 + url, _ := url.Parse(*endpoint + "/push-allowed/") 191 + url.Query().Add(user, user) 192 + url.Query().Add(user, qualifiedRepoName) 193 + 194 + req, err := http.Get(url.String()) 195 + if err != nil { 196 + exitWithLog(fmt.Sprintf("error verifying permissions: %v", err)) 197 + } 198 + 199 + return req.StatusCode == http.StatusNoContent 200 }
+5 -4
knotserver/config/config.go
··· 13 } 14 15 type Server struct { 16 - Host string `env:"HOST, default=0.0.0.0"` 17 - Port int `env:"PORT, default=5555"` 18 - Secret string `env:"SECRET, required"` 19 - DBPath string `env:"DB_PATH, default=knotserver.db"` 20 // This disables signature verification so use with caution. 21 Dev bool `env:"DEV, default=false"` 22 }
··· 13 } 14 15 type Server struct { 16 + Host string `env:"HOST, default=0.0.0.0"` 17 + Port int `env:"PORT, default=5555"` 18 + InternalPort int `env:"PORT, default=5444"` 19 + Secret string `env:"SECRET, required"` 20 + DBPath string `env:"DB_PATH, default=knotserver.db"` 21 // This disables signature verification so use with caution. 22 Dev bool `env:"DEV, default=false"` 23 }
+47
knotserver/internal.go
···
··· 1 + package knotserver 2 + 3 + import ( 4 + "context" 5 + "net/http" 6 + 7 + "github.com/go-chi/chi/v5" 8 + "github.com/sotangled/tangled/knotserver/db" 9 + "github.com/sotangled/tangled/rbac" 10 + ) 11 + 12 + type InternalHandle struct { 13 + db *db.DB 14 + e *rbac.Enforcer 15 + } 16 + 17 + func (h *InternalHandle) PushAllowed(w http.ResponseWriter, r *http.Request) { 18 + user := r.URL.Query().Get("user") 19 + repo := r.URL.Query().Get("repo") 20 + 21 + if user == "" || repo == "" { 22 + w.WriteHeader(http.StatusBadRequest) 23 + return 24 + } 25 + 26 + ok, err := h.e.IsPushAllowed(user, ThisServer, repo) 27 + if err != nil || !ok { 28 + w.WriteHeader(http.StatusForbidden) 29 + return 30 + } 31 + 32 + w.WriteHeader(http.StatusNoContent) 33 + return 34 + } 35 + 36 + func Internal(ctx context.Context, db *db.DB, e *rbac.Enforcer) http.Handler { 37 + r := chi.NewRouter() 38 + 39 + h := InternalHandle{ 40 + db, 41 + e, 42 + } 43 + 44 + r.Get("/push-allowed", h.PushAllowed) 45 + 46 + return r 47 + }
+4
rbac/rbac.go
··· 131 return e.isRole(user, "server:member", domain) 132 } 133 134 // keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin 135 func keyMatch2Func(args ...interface{}) (interface{}, error) { 136 name1 := args[0].(string)
··· 131 return e.isRole(user, "server:member", domain) 132 } 133 134 + func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) { 135 + return e.E.Enforce(user, domain, repo, "repo:push") 136 + } 137 + 138 // keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin 139 func keyMatch2Func(args ...interface{}) (interface{}, error) { 140 name1 := args[0].(string)