Monorepo for Tangled tangled.org

update repoguard to work with rbac

Changed files
+107 -20
cmd
knotserver
repoguard
knotserver
rbac
+7 -1
cmd/knotserver/main.go
··· 46 46 l.Error("failed to setup server", "error", err) 47 47 return 48 48 } 49 - 50 49 addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port) 51 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 + 52 57 l.Info("starting main server", "address", addr) 53 58 l.Error("server error", "error", http.ListenAndServe(addr, mux)) 59 + 54 60 return 55 61 }
+44 -15
cmd/repoguard/main.go
··· 5 5 "flag" 6 6 "fmt" 7 7 "log" 8 + "net/http" 9 + "net/url" 8 10 "os" 9 11 "os/exec" 10 12 "path" ··· 21 23 clientIP string 22 24 23 25 // 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") 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") 27 30 ) 28 31 29 32 func main() { ··· 40 43 } 41 44 } 42 45 43 - if *allowedUser == "" { 46 + if *incomingUser == "" { 44 47 exitWithLog("access denied: no user specified") 45 48 } 46 49 47 50 sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND") 48 51 49 52 logEvent("Connection attempt", map[string]interface{}{ 50 - "user": *allowedUser, 53 + "user": *incomingUser, 51 54 "command": sshCommand, 52 55 "client": clientIP, 53 56 }) ··· 63 66 64 67 gitCommand := cmdParts[0] 65 68 66 - // example.com/repo 67 - handlePath := strings.Trim(cmdParts[1], "'") 68 - repoName := handleToDid(handlePath) 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) 69 80 70 81 validCommands := map[string]bool{ 71 82 "git-receive-pack": true, ··· 76 87 exitWithLog("access denied: invalid git command") 77 88 } 78 89 79 - did := path.Dir(repoName) 80 90 if gitCommand != "git-upload-pack" { 81 - if !isAllowedUser(*allowedUser, did) { 91 + if !isPushPermitted(*incomingUser, qualifiedRepoName) { 82 92 exitWithLog("access denied: user not allowed") 83 93 } 84 94 } 85 95 86 - fullPath := filepath.Join(*baseDirFlag, repoName) 96 + fullPath := filepath.Join(*baseDirFlag, qualifiedRepoName) 87 97 fullPath = filepath.Clean(fullPath) 88 98 89 99 logEvent("Processing command", map[string]interface{}{ 90 - "user": *allowedUser, 100 + "user": *incomingUser, 91 101 "command": gitCommand, 92 102 "repo": repoName, 93 103 "fullPath": fullPath, ··· 104 114 } 105 115 106 116 logEvent("Command completed", map[string]interface{}{ 107 - "user": *allowedUser, 117 + "user": *incomingUser, 108 118 "command": gitCommand, 109 119 "repo": repoName, 110 120 "success": true, 111 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() 112 132 } 113 133 114 134 func handleToDid(handlePath string) string { ··· 166 186 } 167 187 } 168 188 169 - func isAllowedUser(user, did string) bool { 170 - return user == did 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 171 200 }
+5 -4
knotserver/config/config.go
··· 13 13 } 14 14 15 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"` 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"` 20 21 // This disables signature verification so use with caution. 21 22 Dev bool `env:"DEV, default=false"` 22 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 131 return e.isRole(user, "server:member", domain) 132 132 } 133 133 134 + func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) { 135 + return e.E.Enforce(user, domain, repo, "repo:push") 136 + } 137 + 134 138 // keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin 135 139 func keyMatch2Func(args ...interface{}) (interface{}, error) { 136 140 name1 := args[0].(string)