Monorepo for Tangled

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

+203 -116
+124 -24
knotserver/xrpc/create_repo.go
··· 1 1 package xrpc 2 2 3 3 import ( 4 + "context" 4 5 "encoding/json" 5 6 "errors" 6 7 "fmt" 7 8 "net/http" 8 - "path/filepath" 9 + "os" 9 10 "strings" 11 + "time" 10 12 11 - comatproto "github.com/bluesky-social/indigo/api/atproto" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 13 - "github.com/bluesky-social/indigo/xrpc" 14 14 securejoin "github.com/cyphar/filepath-securejoin" 15 15 gogit "github.com/go-git/go-git/v5" 16 16 "tangled.org/core/api/tangled" 17 17 "tangled.org/core/hook" 18 18 "tangled.org/core/knotserver/git" 19 + "tangled.org/core/knotserver/repodid" 19 20 "tangled.org/core/rbac" 20 21 xrpcerr "tangled.org/core/xrpc/errors" 21 22 ) ··· 49 50 return 50 51 } 51 52 52 - rkey := data.Rkey 53 + repoName := data.Name 53 54 54 - ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String()) 55 - if err != nil || ident.Handle.IsInvalidHandle() { 56 - fail(xrpcerr.GenericError(err)) 55 + if repoName == "" { 56 + fail(xrpcerr.GenericError(fmt.Errorf("repository name is required"))) 57 57 return 58 58 } 59 59 60 - xrpcc := xrpc.Client{ 61 - Host: ident.PDSEndpoint(), 60 + defaultBranch := h.Config.Repo.MainBranch 61 + if data.DefaultBranch != nil && *data.DefaultBranch != "" { 62 + defaultBranch = *data.DefaultBranch 62 63 } 63 64 64 - resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 65 - if err != nil { 65 + if err := validateRepoName(repoName); err != nil { 66 + l.Error("creating repo", "error", err.Error()) 66 67 fail(xrpcerr.GenericError(err)) 67 68 return 68 69 } 69 70 70 - repo := resp.Value.Val.(*tangled.Repo) 71 + var repoDid string 72 + var prepared *repodid.PreparedDID 71 73 72 - defaultBranch := h.Config.Repo.MainBranch 73 - if data.DefaultBranch != nil && *data.DefaultBranch != "" { 74 - defaultBranch = *data.DefaultBranch 74 + knotServiceUrl := "https://" + h.Config.Server.Hostname 75 + if h.Config.Server.Dev { 76 + knotServiceUrl = "http://" + h.Config.Server.Hostname 75 77 } 76 78 77 - if err := validateRepoName(repo.Name); err != nil { 78 - l.Error("creating repo", "error", err.Error()) 79 - fail(xrpcerr.GenericError(err)) 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) 80 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 + } 81 140 } 82 141 83 - relativeRepoPath := filepath.Join(actorDid.String(), repo.Name) 84 - repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath) 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 + } 85 159 86 160 if data.Source != nil && *data.Source != "" { 87 161 err = git.Fork(repoPath, *data.Source, h.Config) 88 162 if err != nil { 89 163 l.Error("forking repo", "error", err.Error()) 164 + cleanupAll() 90 165 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 91 166 return 92 167 } ··· 94 169 err = git.InitBare(repoPath, defaultBranch) 95 170 if err != nil { 96 171 l.Error("initializing bare repo", "error", err.Error()) 172 + cleanupAll() 97 173 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 98 174 fail(xrpcerr.RepoExistsError("repository already exists")) 99 175 return 100 - } else { 101 - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 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) 102 187 return 103 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 104 203 } 105 204 } 106 205 107 206 // add perms for this user to access the repo 108 - err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath) 207 + err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, rbacPath) 109 208 if err != nil { 110 209 l.Error("adding repo permissions", "error", err.Error()) 210 + cleanupAll() 111 211 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 112 212 return 113 213 } ··· 120 220 repoPath, 121 221 ) 122 222 123 - w.WriteHeader(http.StatusOK) 223 + writeJson(w, &tangled.RepoCreate_Output{RepoDid: &repoDid}) 124 224 } 125 225 126 226 func validateRepoName(name string) error {
+10 -7
knotserver/xrpc/delete_branch.go
··· 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 11 "tangled.org/core/api/tangled" 13 12 "tangled.org/core/knotserver/git" 14 13 "tangled.org/core/rbac" ··· 57 56 } 58 57 59 58 repo := resp.Value.Val.(*tangled.Repo) 60 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 59 + repoDid, err := x.Db.GetRepoDid(ident.DID.String(), repo.Name) 61 60 if err != nil { 62 - fail(xrpcerr.GenericError(err)) 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) 63 67 return 64 68 } 65 69 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) 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) 68 72 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 69 73 return 70 74 } 71 75 72 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 73 - gr, err := git.PlainOpen(path) 76 + gr, err := git.PlainOpen(repoPath) 74 77 if err != nil { 75 78 fail(xrpcerr.GenericError(err)) 76 79 return
+15 -9
knotserver/xrpc/delete_repo.go
··· 5 5 "fmt" 6 6 "net/http" 7 7 "os" 8 - "path/filepath" 9 8 10 9 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 10 "github.com/bluesky-social/indigo/atproto/syntax" 12 11 "github.com/bluesky-social/indigo/xrpc" 13 - securejoin "github.com/cyphar/filepath-securejoin" 14 12 "tangled.org/core/api/tangled" 15 13 "tangled.org/core/rbac" 16 14 xrpcerr "tangled.org/core/xrpc/errors" ··· 61 59 return 62 60 } 63 61 64 - relativeRepoPath := filepath.Join(did, name) 65 - isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 62 + repoDid, err := x.Db.GetRepoDid(did, name) 66 63 if err != nil { 67 - fail(xrpcerr.GenericError(err)) 64 + fail(xrpcerr.RepoNotFoundError) 68 65 return 69 66 } 70 - if !isDeleteAllowed { 71 - fail(xrpcerr.AccessControlError(actorDid.String())) 67 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 68 + if err != nil { 69 + fail(xrpcerr.RepoNotFoundError) 72 70 return 73 71 } 74 72 75 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 73 + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, repoDid) 76 74 if err != nil { 77 75 fail(xrpcerr.GenericError(err)) 76 + return 77 + } 78 + if !isDeleteAllowed { 79 + fail(xrpcerr.AccessControlError(actorDid.String())) 78 80 return 79 81 } 80 82 ··· 85 87 return 86 88 } 87 89 88 - err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 90 + err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, repoDid) 89 91 if err != nil { 90 92 l.Error("failed to delete repo from enforcer", "error", err.Error()) 91 93 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 92 94 return 95 + } 96 + 97 + if err := x.Db.DeleteRepoKey(repoDid); err != nil { 98 + l.Error("failed to delete repo key", "error", err.Error()) 93 99 } 94 100 95 101 w.WriteHeader(http.StatusOK)
+11 -9
knotserver/xrpc/fork_status.go
··· 7 7 "path/filepath" 8 8 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 10 "tangled.org/core/api/tangled" 12 11 "tangled.org/core/knotserver/git" 13 12 "tangled.org/core/rbac" ··· 51 50 name = filepath.Base(source) 52 51 } 53 52 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) 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) 59 61 return 60 62 } 61 63 62 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 63 - if err != nil { 64 - fail(xrpcerr.GenericError(err)) 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) 65 67 return 66 68 } 67 69
+11 -10
knotserver/xrpc/fork_sync.go
··· 4 4 "encoding/json" 5 5 "fmt" 6 6 "net/http" 7 - "path/filepath" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 9 "tangled.org/core/api/tangled" 12 10 "tangled.org/core/knotserver/git" 13 11 "tangled.org/core/rbac" ··· 42 40 return 43 41 } 44 42 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) 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) 50 51 return 51 52 } 52 53 53 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 54 - if err != nil { 55 - fail(xrpcerr.GenericError(err)) 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) 56 57 return 57 58 } 58 59
+8 -10
knotserver/xrpc/hidden_ref.go
··· 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 11 "tangled.org/core/api/tangled" 13 12 "tangled.org/core/knotserver/git" 14 13 "tangled.org/core/rbac" ··· 63 62 } 64 63 65 64 repo := resp.Value.Val.(*tangled.Repo) 66 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 65 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 67 66 if err != nil { 68 - fail(xrpcerr.GenericError(err)) 67 + fail(xrpcerr.RepoNotFoundError) 69 68 return 70 69 } 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) 70 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 71 + if err != nil { 72 + fail(xrpcerr.RepoNotFoundError) 75 73 return 76 74 } 77 75 78 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 79 - if err != nil { 80 - fail(xrpcerr.GenericError(err)) 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) 81 79 return 82 80 } 83 81
+8 -10
knotserver/xrpc/merge.go
··· 7 7 "net/http" 8 8 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 10 "tangled.org/core/api/tangled" 12 11 "tangled.org/core/knotserver/git" 13 12 "tangled.org/core/patchutil" ··· 43 42 return 44 43 } 45 44 46 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 45 + repoDid, err := x.Db.GetRepoDid(did, name) 47 46 if err != nil { 48 - fail(xrpcerr.GenericError(err)) 47 + fail(xrpcerr.RepoNotFoundError) 49 48 return 50 49 } 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) 50 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 51 + if err != nil { 52 + fail(xrpcerr.RepoNotFoundError) 55 53 return 56 54 } 57 55 58 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 59 - if err != nil { 60 - fail(xrpcerr.GenericError(err)) 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) 61 59 return 62 60 } 63 61
+4 -6
knotserver/xrpc/merge_check.go
··· 6 6 "fmt" 7 7 "net/http" 8 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 9 "tangled.org/core/api/tangled" 11 10 "tangled.org/core/knotserver/git" 12 11 "tangled.org/core/patchutil" ··· 34 33 return 35 34 } 36 35 37 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 36 + repoDid, err := x.Db.GetRepoDid(did, name) 38 37 if err != nil { 39 - fail(xrpcerr.GenericError(err)) 38 + fail(xrpcerr.RepoNotFoundError) 40 39 return 41 40 } 42 - 43 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 41 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 44 42 if err != nil { 45 - fail(xrpcerr.GenericError(err)) 43 + fail(xrpcerr.RepoNotFoundError) 46 44 return 47 45 } 48 46
+9 -6
knotserver/xrpc/set_default_branch.go
··· 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 11 "tangled.org/core/api/tangled" 13 12 "tangled.org/core/knotserver/git" 14 13 "tangled.org/core/rbac" ··· 59 58 } 60 59 61 60 repo := resp.Value.Val.(*tangled.Repo) 62 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 61 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 63 62 if err != nil { 64 - fail(xrpcerr.GenericError(err)) 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) 65 69 return 66 70 } 67 71 68 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 72 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 69 73 l.Error("insufficent permissions", "did", actorDid.String()) 70 74 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 71 75 return 72 76 } 73 77 74 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 75 - gr, err := git.PlainOpen(path) 78 + gr, err := git.PlainOpen(repoPath) 76 79 if err != nil { 77 80 fail(xrpcerr.GenericError(err)) 78 81 return
+3 -25
knotserver/xrpc/xrpc.go
··· 6 6 "net/http" 7 7 "strings" 8 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 9 "tangled.org/core/api/tangled" 11 10 "tangled.org/core/idresolver" 12 11 "tangled.org/core/jetstream" ··· 78 77 return r 79 78 } 80 79 81 - // parseRepoParam parses a repo parameter in 'did/repoName' format and returns 82 - // the full repository path on disk 83 80 func (x *Xrpc) parseRepoParam(repo string) (string, error) { 84 - if repo == "" { 81 + if repo == "" || !strings.HasPrefix(repo, "did:") { 85 82 return "", xrpcerr.NewXrpcError( 86 83 xrpcerr.WithTag("InvalidRequest"), 87 - xrpcerr.WithMessage("missing repo parameter"), 84 + xrpcerr.WithMessage("missing or invalid repo parameter, expected a repo DID"), 88 85 ) 89 86 } 90 87 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) 88 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo) 105 89 if err != nil { 106 90 return "", xrpcerr.RepoNotFoundError 107 91 } 108 - 109 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) 110 - if err != nil { 111 - return "", xrpcerr.RepoNotFoundError 112 - } 113 - 114 92 return repoPath, nil 115 93 } 116 94