Monorepo for Tangled

[WIP] knotserver/xrpc: use repo DID resolution in all handlers

+203 -116
+124 -24
knotserver/xrpc/create_repo.go
··· 1 package xrpc 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 - "path/filepath" 9 "strings" 10 11 - comatproto "github.com/bluesky-social/indigo/api/atproto" 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 - "github.com/bluesky-social/indigo/xrpc" 14 securejoin "github.com/cyphar/filepath-securejoin" 15 gogit "github.com/go-git/go-git/v5" 16 "tangled.org/core/api/tangled" 17 "tangled.org/core/hook" 18 "tangled.org/core/knotserver/git" 19 "tangled.org/core/rbac" 20 xrpcerr "tangled.org/core/xrpc/errors" 21 ) ··· 49 return 50 } 51 52 - rkey := data.Rkey 53 54 - ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String()) 55 - if err != nil || ident.Handle.IsInvalidHandle() { 56 - fail(xrpcerr.GenericError(err)) 57 return 58 } 59 60 - xrpcc := xrpc.Client{ 61 - Host: ident.PDSEndpoint(), 62 } 63 64 - resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 65 - if err != nil { 66 fail(xrpcerr.GenericError(err)) 67 return 68 } 69 70 - repo := resp.Value.Val.(*tangled.Repo) 71 72 - defaultBranch := h.Config.Repo.MainBranch 73 - if data.DefaultBranch != nil && *data.DefaultBranch != "" { 74 - defaultBranch = *data.DefaultBranch 75 } 76 77 - if err := validateRepoName(repo.Name); err != nil { 78 - l.Error("creating repo", "error", err.Error()) 79 - fail(xrpcerr.GenericError(err)) 80 return 81 } 82 83 - relativeRepoPath := filepath.Join(actorDid.String(), repo.Name) 84 - repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath) 85 86 if data.Source != nil && *data.Source != "" { 87 err = git.Fork(repoPath, *data.Source, h.Config) 88 if err != nil { 89 l.Error("forking repo", "error", err.Error()) 90 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 91 return 92 } ··· 94 err = git.InitBare(repoPath, defaultBranch) 95 if err != nil { 96 l.Error("initializing bare repo", "error", err.Error()) 97 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 98 fail(xrpcerr.RepoExistsError("repository already exists")) 99 return 100 - } else { 101 - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 102 return 103 } 104 } 105 } 106 107 // add perms for this user to access the repo 108 - err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath) 109 if err != nil { 110 l.Error("adding repo permissions", "error", err.Error()) 111 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 112 return 113 } ··· 120 repoPath, 121 ) 122 123 - w.WriteHeader(http.StatusOK) 124 } 125 126 func validateRepoName(name string) error {
··· 1 package xrpc 2 3 import ( 4 + "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 + "os" 10 "strings" 11 + "time" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 securejoin "github.com/cyphar/filepath-securejoin" 15 gogit "github.com/go-git/go-git/v5" 16 "tangled.org/core/api/tangled" 17 "tangled.org/core/hook" 18 "tangled.org/core/knotserver/git" 19 + "tangled.org/core/knotserver/repodid" 20 "tangled.org/core/rbac" 21 xrpcerr "tangled.org/core/xrpc/errors" 22 ) ··· 50 return 51 } 52 53 + repoName := data.Name 54 55 + if repoName == "" { 56 + fail(xrpcerr.GenericError(fmt.Errorf("repository name is required"))) 57 return 58 } 59 60 + defaultBranch := h.Config.Repo.MainBranch 61 + if data.DefaultBranch != nil && *data.DefaultBranch != "" { 62 + defaultBranch = *data.DefaultBranch 63 } 64 65 + if err := validateRepoName(repoName); err != nil { 66 + l.Error("creating repo", "error", err.Error()) 67 fail(xrpcerr.GenericError(err)) 68 return 69 } 70 71 + var repoDid string 72 + var prepared *repodid.PreparedDID 73 74 + knotServiceUrl := "https://" + h.Config.Server.Hostname 75 + if h.Config.Server.Dev { 76 + knotServiceUrl = "http://" + h.Config.Server.Hostname 77 } 78 79 + switch { 80 + case data.RepoDid != nil && strings.HasPrefix(*data.RepoDid, "did:web:"): 81 + if err := repodid.VerifyRepoDIDWeb(r.Context(), h.Resolver, *data.RepoDid, knotServiceUrl); err != nil { 82 + l.Error("verifying did:web", "error", err.Error()) 83 + writeError(w, xrpcerr.GenericError(err), http.StatusBadRequest) 84 + return 85 + } 86 + 87 + exists, err := h.Db.RepoDidExists(*data.RepoDid) 88 + if err != nil { 89 + l.Error("checking did:web uniqueness", "error", err.Error()) 90 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 91 + return 92 + } 93 + if exists { 94 + writeError(w, xrpcerr.GenericError(fmt.Errorf("did:web %s is already in use on this knot", *data.RepoDid)), http.StatusConflict) 95 + return 96 + } 97 + 98 + repoDid = *data.RepoDid 99 + 100 + case data.RepoDid != nil && *data.RepoDid != "": 101 + writeError(w, xrpcerr.GenericError(fmt.Errorf("only did:web is accepted as a user-provided repo DID; did:plc is auto-generated")), http.StatusBadRequest) 102 return 103 + 104 + default: 105 + existingDid, dbErr := h.Db.GetRepoDid(actorDid.String(), repoName) 106 + if dbErr == nil && existingDid != "" { 107 + didRepoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, existingDid) 108 + if _, statErr := os.Stat(didRepoPath); statErr == nil { 109 + l.Info("repo already exists from previous attempt", "repoDid", existingDid) 110 + output := tangled.RepoCreate_Output{RepoDid: &existingDid} 111 + writeJson(w, &output) 112 + return 113 + } 114 + l.Warn("stale repo key found without directory, cleaning up", "repoDid", existingDid) 115 + if delErr := h.Db.DeleteRepoKey(existingDid); delErr != nil { 116 + l.Error("failed to clean up stale repo key", "repoDid", existingDid, "error", delErr.Error()) 117 + writeError(w, xrpcerr.GenericError(fmt.Errorf("failed to clean up stale state, retry later")), http.StatusInternalServerError) 118 + return 119 + } 120 + } 121 + 122 + var prepErr error 123 + prepared, prepErr = repodid.PrepareRepoDID(h.Config.Server.PlcUrl, knotServiceUrl) 124 + if prepErr != nil { 125 + l.Error("preparing repo DID", "error", prepErr.Error()) 126 + writeError(w, xrpcerr.GenericError(prepErr), http.StatusInternalServerError) 127 + return 128 + } 129 + repoDid = prepared.RepoDid 130 + 131 + if err := h.Db.StoreRepoKey(repoDid, prepared.SigningKeyRaw, actorDid.String(), repoName); err != nil { 132 + if strings.Contains(err.Error(), "UNIQUE constraint failed") { 133 + writeError(w, xrpcerr.GenericError(fmt.Errorf("repository %s already being created", repoName)), http.StatusConflict) 134 + return 135 + } 136 + l.Error("claiming repo key slot", "error", err.Error()) 137 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 138 + return 139 + } 140 } 141 142 + l = l.With("repoDid", repoDid) 143 + 144 + repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, repoDid) 145 + rbacPath := repoDid 146 + 147 + cleanup := func() { 148 + if rmErr := os.RemoveAll(repoPath); rmErr != nil { 149 + l.Error("failed to clean up repo directory", "path", repoPath, "error", rmErr.Error()) 150 + } 151 + } 152 + 153 + cleanupAll := func() { 154 + cleanup() 155 + if delErr := h.Db.DeleteRepoKey(repoDid); delErr != nil { 156 + l.Error("failed to clean up repo key", "error", delErr.Error()) 157 + } 158 + } 159 160 if data.Source != nil && *data.Source != "" { 161 err = git.Fork(repoPath, *data.Source, h.Config) 162 if err != nil { 163 l.Error("forking repo", "error", err.Error()) 164 + cleanupAll() 165 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 166 return 167 } ··· 169 err = git.InitBare(repoPath, defaultBranch) 170 if err != nil { 171 l.Error("initializing bare repo", "error", err.Error()) 172 + cleanupAll() 173 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 174 fail(xrpcerr.RepoExistsError("repository already exists")) 175 return 176 + } 177 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 178 + return 179 + } 180 + } 181 + 182 + if data.RepoDid != nil && strings.HasPrefix(*data.RepoDid, "did:web:") { 183 + if err := h.Db.StoreRepoDidWeb(repoDid, actorDid.String(), repoName); err != nil { 184 + cleanupAll() 185 + if strings.Contains(err.Error(), "UNIQUE constraint failed") { 186 + writeError(w, xrpcerr.GenericError(fmt.Errorf("did:web %s is already in use", repoDid)), http.StatusConflict) 187 return 188 } 189 + l.Error("storing did:web repo entry", "error", err.Error()) 190 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 191 + return 192 + } 193 + } 194 + 195 + if prepared != nil { 196 + plcCtx, plcCancel := context.WithTimeout(context.Background(), 30*time.Second) 197 + defer plcCancel() 198 + if err := prepared.Submit(plcCtx); err != nil { 199 + l.Error("submitting to PLC directory", "error", err.Error()) 200 + cleanupAll() 201 + writeError(w, xrpcerr.GenericError(fmt.Errorf("PLC directory submission failed: %w", err)), http.StatusInternalServerError) 202 + return 203 } 204 } 205 206 // add perms for this user to access the repo 207 + err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, rbacPath) 208 if err != nil { 209 l.Error("adding repo permissions", "error", err.Error()) 210 + cleanupAll() 211 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 212 return 213 } ··· 220 repoPath, 221 ) 222 223 + writeJson(w, &tangled.RepoCreate_Output{RepoDid: &repoDid}) 224 } 225 226 func validateRepoName(name string) error {
+10 -7
knotserver/xrpc/delete_branch.go
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/knotserver/git" 14 "tangled.org/core/rbac" ··· 57 } 58 59 repo := resp.Value.Val.(*tangled.Repo) 60 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 61 if err != nil { 62 - fail(xrpcerr.GenericError(err)) 63 return 64 } 65 66 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 67 - l.Error("insufficent permissions", "did", actorDid.String(), "repo", didPath) 68 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 69 return 70 } 71 72 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 73 - gr, err := git.PlainOpen(path) 74 if err != nil { 75 fail(xrpcerr.GenericError(err)) 76 return
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 56 } 57 58 repo := resp.Value.Val.(*tangled.Repo) 59 + repoDid, err := x.Db.GetRepoDid(ident.DID.String(), repo.Name) 60 if err != nil { 61 + fail(xrpcerr.RepoNotFoundError) 62 + return 63 + } 64 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 65 + if err != nil { 66 + fail(xrpcerr.RepoNotFoundError) 67 return 68 } 69 70 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 71 + l.Error("insufficent permissions", "did", actorDid.String(), "repo", repoDid) 72 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 73 return 74 } 75 76 + gr, err := git.PlainOpen(repoPath) 77 if err != nil { 78 fail(xrpcerr.GenericError(err)) 79 return
+15 -9
knotserver/xrpc/delete_repo.go
··· 5 "fmt" 6 "net/http" 7 "os" 8 - "path/filepath" 9 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 "github.com/bluesky-social/indigo/xrpc" 13 - securejoin "github.com/cyphar/filepath-securejoin" 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/rbac" 16 xrpcerr "tangled.org/core/xrpc/errors" ··· 61 return 62 } 63 64 - relativeRepoPath := filepath.Join(did, name) 65 - isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 66 if err != nil { 67 - fail(xrpcerr.GenericError(err)) 68 return 69 } 70 - if !isDeleteAllowed { 71 - fail(xrpcerr.AccessControlError(actorDid.String())) 72 return 73 } 74 75 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 76 if err != nil { 77 fail(xrpcerr.GenericError(err)) 78 return 79 } 80 ··· 85 return 86 } 87 88 - err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 89 if err != nil { 90 l.Error("failed to delete repo from enforcer", "error", err.Error()) 91 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 92 return 93 } 94 95 w.WriteHeader(http.StatusOK)
··· 5 "fmt" 6 "net/http" 7 "os" 8 9 comatproto "github.com/bluesky-social/indigo/api/atproto" 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 "github.com/bluesky-social/indigo/xrpc" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/rbac" 14 xrpcerr "tangled.org/core/xrpc/errors" ··· 59 return 60 } 61 62 + repoDid, err := x.Db.GetRepoDid(did, name) 63 if err != nil { 64 + fail(xrpcerr.RepoNotFoundError) 65 return 66 } 67 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 68 + if err != nil { 69 + fail(xrpcerr.RepoNotFoundError) 70 return 71 } 72 73 + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, repoDid) 74 if err != nil { 75 fail(xrpcerr.GenericError(err)) 76 + return 77 + } 78 + if !isDeleteAllowed { 79 + fail(xrpcerr.AccessControlError(actorDid.String())) 80 return 81 } 82 ··· 87 return 88 } 89 90 + err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, repoDid) 91 if err != nil { 92 l.Error("failed to delete repo from enforcer", "error", err.Error()) 93 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 94 return 95 + } 96 + 97 + if err := x.Db.DeleteRepoKey(repoDid); err != nil { 98 + l.Error("failed to delete repo key", "error", err.Error()) 99 } 100 101 w.WriteHeader(http.StatusOK)
+11 -9
knotserver/xrpc/fork_status.go
··· 7 "path/filepath" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 51 name = filepath.Base(source) 52 } 53 54 - relativeRepoPath := filepath.Join(did, name) 55 - 56 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 57 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 58 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 59 return 60 } 61 62 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 63 - if err != nil { 64 - fail(xrpcerr.GenericError(err)) 65 return 66 } 67
··· 7 "path/filepath" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/knotserver/git" 12 "tangled.org/core/rbac" ··· 50 name = filepath.Base(source) 51 } 52 53 + repoDid, err := x.Db.GetRepoDid(did, name) 54 + if err != nil { 55 + fail(xrpcerr.RepoNotFoundError) 56 + return 57 + } 58 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 59 + if err != nil { 60 + fail(xrpcerr.RepoNotFoundError) 61 return 62 } 63 64 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 65 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 66 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 67 return 68 } 69
+11 -10
knotserver/xrpc/fork_sync.go
··· 4 "encoding/json" 5 "fmt" 6 "net/http" 7 - "path/filepath" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 42 return 43 } 44 45 - relativeRepoPath := filepath.Join(did, name) 46 - 47 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 48 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 49 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 50 return 51 } 52 53 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 54 - if err != nil { 55 - fail(xrpcerr.GenericError(err)) 56 return 57 } 58
··· 4 "encoding/json" 5 "fmt" 6 "net/http" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/knotserver/git" 11 "tangled.org/core/rbac" ··· 40 return 41 } 42 43 + repoDid, err := x.Db.GetRepoDid(did, name) 44 + if err != nil { 45 + fail(xrpcerr.RepoNotFoundError) 46 + return 47 + } 48 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 49 + if err != nil { 50 + fail(xrpcerr.RepoNotFoundError) 51 return 52 } 53 54 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 55 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 56 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 57 return 58 } 59
+8 -10
knotserver/xrpc/hidden_ref.go
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/knotserver/git" 14 "tangled.org/core/rbac" ··· 63 } 64 65 repo := resp.Value.Val.(*tangled.Repo) 66 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 67 if err != nil { 68 - fail(xrpcerr.GenericError(err)) 69 return 70 } 71 - 72 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 73 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", didPath) 74 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 75 return 76 } 77 78 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 79 - if err != nil { 80 - fail(xrpcerr.GenericError(err)) 81 return 82 } 83
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 62 } 63 64 repo := resp.Value.Val.(*tangled.Repo) 65 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 66 if err != nil { 67 + fail(xrpcerr.RepoNotFoundError) 68 return 69 } 70 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 71 + if err != nil { 72 + fail(xrpcerr.RepoNotFoundError) 73 return 74 } 75 76 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 77 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 78 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 79 return 80 } 81
+8 -10
knotserver/xrpc/merge.go
··· 7 "net/http" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/patchutil" ··· 43 return 44 } 45 46 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 47 if err != nil { 48 - fail(xrpcerr.GenericError(err)) 49 return 50 } 51 - 52 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 53 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 54 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 55 return 56 } 57 58 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 59 - if err != nil { 60 - fail(xrpcerr.GenericError(err)) 61 return 62 } 63
··· 7 "net/http" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/knotserver/git" 12 "tangled.org/core/patchutil" ··· 42 return 43 } 44 45 + repoDid, err := x.Db.GetRepoDid(did, name) 46 if err != nil { 47 + fail(xrpcerr.RepoNotFoundError) 48 return 49 } 50 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 51 + if err != nil { 52 + fail(xrpcerr.RepoNotFoundError) 53 return 54 } 55 56 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 57 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 58 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 59 return 60 } 61
+4 -6
knotserver/xrpc/merge_check.go
··· 6 "fmt" 7 "net/http" 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/knotserver/git" 12 "tangled.org/core/patchutil" ··· 34 return 35 } 36 37 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 38 if err != nil { 39 - fail(xrpcerr.GenericError(err)) 40 return 41 } 42 - 43 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 44 if err != nil { 45 - fail(xrpcerr.GenericError(err)) 46 return 47 } 48
··· 6 "fmt" 7 "net/http" 8 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/knotserver/git" 11 "tangled.org/core/patchutil" ··· 33 return 34 } 35 36 + repoDid, err := x.Db.GetRepoDid(did, name) 37 if err != nil { 38 + fail(xrpcerr.RepoNotFoundError) 39 return 40 } 41 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 42 if err != nil { 43 + fail(xrpcerr.RepoNotFoundError) 44 return 45 } 46
+9 -6
knotserver/xrpc/set_default_branch.go
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/knotserver/git" 14 "tangled.org/core/rbac" ··· 59 } 60 61 repo := resp.Value.Val.(*tangled.Repo) 62 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 63 if err != nil { 64 - fail(xrpcerr.GenericError(err)) 65 return 66 } 67 68 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 69 l.Error("insufficent permissions", "did", actorDid.String()) 70 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 71 return 72 } 73 74 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 75 - gr, err := git.PlainOpen(path) 76 if err != nil { 77 fail(xrpcerr.GenericError(err)) 78 return
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 58 } 59 60 repo := resp.Value.Val.(*tangled.Repo) 61 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 62 if err != nil { 63 + fail(xrpcerr.RepoNotFoundError) 64 + return 65 + } 66 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 67 + if err != nil { 68 + fail(xrpcerr.RepoNotFoundError) 69 return 70 } 71 72 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 73 l.Error("insufficent permissions", "did", actorDid.String()) 74 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 75 return 76 } 77 78 + gr, err := git.PlainOpen(repoPath) 79 if err != nil { 80 fail(xrpcerr.GenericError(err)) 81 return
+3 -25
knotserver/xrpc/xrpc.go
··· 6 "net/http" 7 "strings" 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/idresolver" 12 "tangled.org/core/jetstream" ··· 78 return r 79 } 80 81 - // parseRepoParam parses a repo parameter in 'did/repoName' format and returns 82 - // the full repository path on disk 83 func (x *Xrpc) parseRepoParam(repo string) (string, error) { 84 - if repo == "" { 85 return "", xrpcerr.NewXrpcError( 86 xrpcerr.WithTag("InvalidRequest"), 87 - xrpcerr.WithMessage("missing repo parameter"), 88 ) 89 } 90 91 - // Parse repo string (did/repoName format) 92 - parts := strings.SplitN(repo, "/", 2) 93 - if len(parts) != 2 { 94 - return "", xrpcerr.NewXrpcError( 95 - xrpcerr.WithTag("InvalidRequest"), 96 - xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"), 97 - ) 98 - } 99 - 100 - did := parts[0] 101 - repoName := parts[1] 102 - 103 - // Construct repository path using the same logic as didPath 104 - didRepoPath, err := securejoin.SecureJoin(did, repoName) 105 if err != nil { 106 return "", xrpcerr.RepoNotFoundError 107 } 108 - 109 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) 110 - if err != nil { 111 - return "", xrpcerr.RepoNotFoundError 112 - } 113 - 114 return repoPath, nil 115 } 116
··· 6 "net/http" 7 "strings" 8 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/idresolver" 11 "tangled.org/core/jetstream" ··· 77 return r 78 } 79 80 func (x *Xrpc) parseRepoParam(repo string) (string, error) { 81 + if repo == "" || !strings.HasPrefix(repo, "did:") { 82 return "", xrpcerr.NewXrpcError( 83 xrpcerr.WithTag("InvalidRequest"), 84 + xrpcerr.WithMessage("missing or invalid repo parameter, expected a repo DID"), 85 ) 86 } 87 88 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo) 89 if err != nil { 90 return "", xrpcerr.RepoNotFoundError 91 } 92 return repoPath, nil 93 } 94