forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

update repoguard to work with rbac

Changed files
+107 -20
cmd
knotserver
repoguard
knotserver
rbac
+7 -1
cmd/knotserver/main.go
···
l.Error("failed to setup server", "error", err)
return
}
-
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
+
imux := knotserver.Internal(ctx, db, e)
+
iaddr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.InternalPort)
+
+
l.Info("starting internal server", "address", iaddr)
+
go http.ListenAndServe(iaddr, imux)
+
l.Info("starting main server", "address", addr)
l.Error("server error", "error", http.ListenAndServe(addr, mux))
+
return
}
+44 -15
cmd/repoguard/main.go
···
"flag"
"fmt"
"log"
+
"net/http"
+
"net/url"
"os"
"os/exec"
"path"
···
clientIP string
// Command line flags
-
allowedUser = flag.String("user", "", "Allowed git user")
-
baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories")
-
logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file")
+
incomingUser = flag.String("user", "", "Allowed git user")
+
baseDirFlag = flag.String("base-dir", "/home/git", "Base directory for git repositories")
+
logPathFlag = flag.String("log-path", "/var/log/git-wrapper.log", "Path to log file")
+
endpoint = flag.String("internal-api", "http://localhost:5555", "Internal API endpoint")
)
func main() {
···
}
}
-
if *allowedUser == "" {
+
if *incomingUser == "" {
exitWithLog("access denied: no user specified")
}
sshCommand := os.Getenv("SSH_ORIGINAL_COMMAND")
logEvent("Connection attempt", map[string]interface{}{
-
"user": *allowedUser,
+
"user": *incomingUser,
"command": sshCommand,
"client": clientIP,
})
···
gitCommand := cmdParts[0]
-
// example.com/repo
-
handlePath := strings.Trim(cmdParts[1], "'")
-
repoName := handleToDid(handlePath)
+
// did:foo/repo-name or
+
// handle/repo-name
+
components := filepath.SplitList(cmdParts[2])
+
if len(components) != 2 {
+
exitWithLog("invalid repo format, needs <user>/<repo>")
+
}
+
+
didOrHandle := components[0]
+
did := resolveToDid(didOrHandle)
+
repoName := components[1]
+
qualifiedRepoName := filepath.Join(did, repoName)
validCommands := map[string]bool{
"git-receive-pack": true,
···
exitWithLog("access denied: invalid git command")
}
-
did := path.Dir(repoName)
if gitCommand != "git-upload-pack" {
-
if !isAllowedUser(*allowedUser, did) {
+
if !isPushPermitted(*incomingUser, qualifiedRepoName) {
exitWithLog("access denied: user not allowed")
}
}
-
fullPath := filepath.Join(*baseDirFlag, repoName)
+
fullPath := filepath.Join(*baseDirFlag, qualifiedRepoName)
fullPath = filepath.Clean(fullPath)
logEvent("Processing command", map[string]interface{}{
-
"user": *allowedUser,
+
"user": *incomingUser,
"command": gitCommand,
"repo": repoName,
"fullPath": fullPath,
···
}
logEvent("Command completed", map[string]interface{}{
-
"user": *allowedUser,
+
"user": *incomingUser,
"command": gitCommand,
"repo": repoName,
"success": true,
})
+
}
+
+
func resolveToDid(didOrHandle string) string {
+
ident, err := auth.ResolveIdent(context.Background(), didOrHandle)
+
if err != nil {
+
exitWithLog(fmt.Sprintf("error resolving handle: %v", err))
+
}
+
+
// did:plc:foobarbaz/repo
+
return ident.DID.String()
}
func handleToDid(handlePath string) string {
···
}
}
-
func isAllowedUser(user, did string) bool {
-
return user == did
+
func isPushPermitted(user, qualifiedRepoName string) bool {
+
url, _ := url.Parse(*endpoint + "/push-allowed/")
+
url.Query().Add(user, user)
+
url.Query().Add(user, qualifiedRepoName)
+
+
req, err := http.Get(url.String())
+
if err != nil {
+
exitWithLog(fmt.Sprintf("error verifying permissions: %v", err))
+
}
+
+
return req.StatusCode == http.StatusNoContent
}
+5 -4
knotserver/config/config.go
···
}
type Server struct {
-
Host string `env:"HOST, default=0.0.0.0"`
-
Port int `env:"PORT, default=5555"`
-
Secret string `env:"SECRET, required"`
-
DBPath string `env:"DB_PATH, default=knotserver.db"`
+
Host string `env:"HOST, default=0.0.0.0"`
+
Port int `env:"PORT, default=5555"`
+
InternalPort int `env:"PORT, default=5444"`
+
Secret string `env:"SECRET, required"`
+
DBPath string `env:"DB_PATH, default=knotserver.db"`
// This disables signature verification so use with caution.
Dev bool `env:"DEV, default=false"`
}
+47
knotserver/internal.go
···
+
package knotserver
+
+
import (
+
"context"
+
"net/http"
+
+
"github.com/go-chi/chi/v5"
+
"github.com/sotangled/tangled/knotserver/db"
+
"github.com/sotangled/tangled/rbac"
+
)
+
+
type InternalHandle struct {
+
db *db.DB
+
e *rbac.Enforcer
+
}
+
+
func (h *InternalHandle) PushAllowed(w http.ResponseWriter, r *http.Request) {
+
user := r.URL.Query().Get("user")
+
repo := r.URL.Query().Get("repo")
+
+
if user == "" || repo == "" {
+
w.WriteHeader(http.StatusBadRequest)
+
return
+
}
+
+
ok, err := h.e.IsPushAllowed(user, ThisServer, repo)
+
if err != nil || !ok {
+
w.WriteHeader(http.StatusForbidden)
+
return
+
}
+
+
w.WriteHeader(http.StatusNoContent)
+
return
+
}
+
+
func Internal(ctx context.Context, db *db.DB, e *rbac.Enforcer) http.Handler {
+
r := chi.NewRouter()
+
+
h := InternalHandle{
+
db,
+
e,
+
}
+
+
r.Get("/push-allowed", h.PushAllowed)
+
+
return r
+
}
+4
rbac/rbac.go
···
return e.isRole(user, "server:member", domain)
}
+
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
+
return e.E.Enforce(user, domain, repo, "repo:push")
+
}
+
// keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin
func keyMatch2Func(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)