forked from tangled.org/core
this repo has no description

appview: rework repo delete

requires record deletion first

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 0dd491e1 d73d335b

verified
Changed files
+111 -82
api
tangled
appview
repo
knotserver
lexicons
rbac
xrpc
errors
+2
api/tangled/repodelete.go
··· 20 20 Did string `json:"did" cborgen:"did"` 21 21 // name: Name of the repository to delete 22 22 Name string `json:"name" cborgen:"name"` 23 + // rkey: Rkey of the repository record 24 + Rkey string `json:"rkey" cborgen:"rkey"` 23 25 } 24 26 25 27 // RepoDelete calls the XRPC method "sh.tangled.repo.delete".
+1
appview/repo/repo.go
··· 972 972 &tangled.RepoDelete_Input{ 973 973 Did: f.OwnerDid(), 974 974 Name: f.Name, 975 + Rkey: f.Rkey, 975 976 }, 976 977 ) 977 978 if err := xrpcclient.HandleXrpcErr(err); err != nil {
+93 -79
knotserver/xrpc/delete_repo.go
··· 1 1 package xrpc 2 2 3 - // import ( 4 - // "encoding/json" 5 - // "fmt" 6 - // "net/http" 7 - // "os" 8 - // "path/filepath" 9 - // 10 - // "github.com/bluesky-social/indigo/atproto/syntax" 11 - // securejoin "github.com/cyphar/filepath-securejoin" 12 - // "tangled.sh/tangled.sh/core/api/tangled" 13 - // "tangled.sh/tangled.sh/core/rbac" 14 - // xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 15 - // ) 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "os" 8 + "path/filepath" 16 9 17 - // func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) { 18 - // l := x.Logger.With("handler", "DeleteRepo") 19 - // fail := func(e xrpcerr.XrpcError) { 20 - // l.Error("failed", "kind", e.Tag, "error", e.Message) 21 - // writeError(w, e, http.StatusBadRequest) 22 - // } 23 - // 24 - // actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 25 - // if !ok { 26 - // fail(xrpcerr.MissingActorDidError) 27 - // return 28 - // } 29 - // 30 - // isMember, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer) 31 - // if err != nil { 32 - // fail(xrpcerr.GenericError(err)) 33 - // return 34 - // } 35 - // if !isMember { 36 - // fail(xrpcerr.AccessControlError(actorDid.String())) 37 - // return 38 - // } 39 - // 40 - // var data tangled.RepoDelete_Input 41 - // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 42 - // fail(xrpcerr.GenericError(err)) 43 - // return 44 - // } 45 - // 46 - // did := data.Did 47 - // name := data.Name 48 - // 49 - // if did == "" || name == "" { 50 - // fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 51 - // return 52 - // } 53 - // 54 - // relativeRepoPath := filepath.Join(did, name) 55 - // if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 56 - // l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 57 - // writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 58 - // return 59 - // } 60 - // 61 - // repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 62 - // if err != nil { 63 - // fail(xrpcerr.GenericError(err)) 64 - // return 65 - // } 66 - // 67 - // err = os.RemoveAll(repoPath) 68 - // if err != nil { 69 - // l.Error("deleting repo", "error", err.Error()) 70 - // writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 71 - // return 72 - // } 73 - // 74 - // err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 75 - // if err != nil { 76 - // l.Error("failed to delete repo from enforcer", "error", err.Error()) 77 - // writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 78 - // return 79 - // } 80 - // 81 - // w.WriteHeader(http.StatusOK) 82 - // } 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.sh/tangled.sh/core/api/tangled" 15 + "tangled.sh/tangled.sh/core/rbac" 16 + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 17 + ) 18 + 19 + func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) { 20 + l := x.Logger.With("handler", "DeleteRepo") 21 + fail := func(e xrpcerr.XrpcError) { 22 + l.Error("failed", "kind", e.Tag, "error", e.Message) 23 + writeError(w, e, http.StatusBadRequest) 24 + } 25 + 26 + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 27 + if !ok { 28 + fail(xrpcerr.MissingActorDidError) 29 + return 30 + } 31 + 32 + var data tangled.RepoDelete_Input 33 + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 34 + fail(xrpcerr.GenericError(err)) 35 + return 36 + } 37 + 38 + did := data.Did 39 + name := data.Name 40 + rkey := data.Rkey 41 + 42 + if did == "" || name == "" { 43 + fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 44 + return 45 + } 46 + 47 + ident, err := x.Resolver.ResolveIdent(r.Context(), actorDid.String()) 48 + if err != nil || ident.Handle.IsInvalidHandle() { 49 + fail(xrpcerr.GenericError(err)) 50 + return 51 + } 52 + 53 + xrpcc := xrpc.Client{ 54 + Host: ident.PDSEndpoint(), 55 + } 56 + 57 + // ensure that the record does not exists 58 + _, err = comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 59 + if err == nil { 60 + fail(xrpcerr.RecordExistsError(rkey)) 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 + 81 + err = os.RemoveAll(repoPath) 82 + if err != nil { 83 + l.Error("deleting repo", "error", err.Error()) 84 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 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) 96 + }
+1
knotserver/xrpc/xrpc.go
··· 37 37 38 38 r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) 39 39 r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo) 40 + r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo) 40 41 r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus) 41 42 r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync) 42 43 r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)
+5 -1
lexicons/repo/delete.json
··· 9 9 "encoding": "application/json", 10 10 "schema": { 11 11 "type": "object", 12 - "required": ["did", "name"], 12 + "required": ["did", "name", "rkey"], 13 13 "properties": { 14 14 "did": { 15 15 "type": "string", ··· 19 19 "name": { 20 20 "type": "string", 21 21 "description": "Name of the repository to delete" 22 + }, 23 + "rkey": { 24 + "type": "string", 25 + "description": "Rkey of the repository record" 22 26 } 23 27 } 24 28 }
+2 -2
rbac/rbac.go
··· 281 281 return e.E.Enforce(user, domain, domain, "repo:create") 282 282 } 283 283 284 - func (e *Enforcer) IsRepoDeleteAllowed(user, domain string) (bool, error) { 285 - return e.E.Enforce(user, domain, domain, "repo:delete") 284 + func (e *Enforcer) IsRepoDeleteAllowed(user, domain, repo string) (bool, error) { 285 + return e.E.Enforce(user, domain, repo, "repo:delete") 286 286 } 287 287 288 288 func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
+7
xrpc/errors/errors.go
··· 86 86 ) 87 87 } 88 88 89 + var RecordExistsError = func(r string) XrpcError { 90 + return NewXrpcError( 91 + WithTag("RecordExists"), 92 + WithError(fmt.Errorf("repo already exists: %s", r)), 93 + ) 94 + } 95 + 89 96 func GenericError(err error) XrpcError { 90 97 return NewXrpcError( 91 98 WithTag("Generic"),