From 6c27a7472e1fc93897dd5c8fc8d2782d405abe95 Mon Sep 17 00:00:00 2001 From: "oppili.bsky.social" Date: Mon, 7 Apr 2025 10:02:33 +0000 Subject: [PATCH] feat: repo deletion this capability has existed in knots since the first release --- appview/db/profile.go | 5 - appview/db/repos.go | 4 +- appview/pages/pages.go | 8 ++ appview/pages/templates/repo/settings.html | 17 +++- appview/state/repo.go | 106 +++++++++++++++++++++ appview/state/router.go | 1 + rbac/rbac.go | 77 +++++++++++---- 7 files changed, 188 insertions(+), 30 deletions(-) diff --git a/appview/db/profile.go b/appview/db/profile.go index de8ddb1a..2e2fccb1 100644 --- a/appview/db/profile.go +++ b/appview/db/profile.go @@ -2,7 +2,6 @@ package db import ( "fmt" - "log" "sort" "time" ) @@ -65,13 +64,9 @@ func MakeProfileTimeline(e Execer, forDid string) ([]ProfileTimelineEvent, error return timeline, fmt.Errorf("error getting all repos by did: %w", err) } - log.Println(repos) - for _, repo := range repos { var sourceRepo *Repo - log.Println("name", repo.Name) if repo.Source != "" { - log.Println("source", repo.Source) sourceRepo, err = GetRepoByAtUri(e, repo.Source) if err != nil { return nil, err diff --git a/appview/db/repos.go b/appview/db/repos.go index 23927ec2..915f48d5 100644 --- a/appview/db/repos.go +++ b/appview/db/repos.go @@ -176,8 +176,8 @@ func AddRepo(e Execer, repo *Repo) error { return err } -func RemoveRepo(e Execer, did, name, rkey string) error { - _, err := e.Exec(`delete from repos where did = ? and name = ? and rkey = ?`, did, name, rkey) +func RemoveRepo(e Execer, did, name string) error { + _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) return err } diff --git a/appview/pages/pages.go b/appview/pages/pages.go index fdfaa64a..747246c2 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -246,6 +246,14 @@ func (r RolesInRepo) SettingsAllowed() bool { return slices.Contains(r.Roles, "repo:settings") } +func (r RolesInRepo) CollaboratorInviteAllowed() bool { + return slices.Contains(r.Roles, "repo:invite") +} + +func (r RolesInRepo) RepoDeleteAllowed() bool { + return slices.Contains(r.Roles, "repo:delete") +} + func (r RolesInRepo) IsOwner() bool { return slices.Contains(r.Roles, "repo:owner") } diff --git a/appview/pages/templates/repo/settings.html b/appview/pages/templates/repo/settings.html index ceef3b21..b3369d74 100644 --- a/appview/pages/templates/repo/settings.html +++ b/appview/pages/templates/repo/settings.html @@ -22,7 +22,7 @@ {{ end }} - {{ if .IsCollaboratorInviteAllowed }} + {{ if .RepoInfo.Roles.CollaboratorInviteAllowed }}
- - {{ range .Branches }}
+ + {{ if .RepoInfo.Roles.RepoDeleteAllowed }} +
+ + + + Deleting a repository is irreversible and permanent. + +
+ {{ end }} + {{ end }} diff --git a/appview/state/repo.go b/appview/state/repo.go index 905cc9f8..40b10ee8 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -596,6 +596,112 @@ func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { } +func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { + user := s.auth.GetUser(r) + + f, err := fullyResolvedRepo(r) + if err != nil { + log.Println("failed to get repo and knot", err) + return + } + + // remove record from pds + xrpcClient, _ := s.auth.AuthorizedClient(r) + repoRkey := f.RepoAt.RecordKey().String() + _, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{ + Collection: tangled.RepoNSID, + Repo: user.Did, + Rkey: repoRkey, + }) + if err != nil { + log.Printf("failed to delete record: %s", err) + s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") + return + } + log.Println("removed repo record ", f.RepoAt.String()) + + secret, err := db.GetRegistrationKey(s.db, f.Knot) + if err != nil { + log.Printf("no key found for domain %s: %s\n", f.Knot, err) + return + } + + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) + if err != nil { + log.Println("failed to create client to ", f.Knot) + return + } + + ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) + if err != nil { + log.Printf("failed to make request to %s: %s", f.Knot, err) + return + } + + if ksResp.StatusCode != http.StatusNoContent { + log.Println("failed to remove repo from knot, continuing anyway ", f.Knot) + } else { + log.Println("removed repo from knot ", f.Knot) + } + + tx, err := s.db.BeginTx(r.Context(), nil) + if err != nil { + log.Println("failed to start tx") + w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) + return + } + defer func() { + tx.Rollback() + err = s.enforcer.E.LoadPolicy() + if err != nil { + log.Println("failed to rollback policies") + } + }() + + // remove collaborator RBAC + repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot) + if err != nil { + s.pages.Notice(w, "settings-delete", "Failed to remove collaborators") + return + } + for _, c := range repoCollaborators { + did := c[0] + s.enforcer.RemoveCollaborator(did, f.Knot, f.OwnerSlashRepo()) + } + log.Println("removed collaborators") + + // remove repo RBAC + err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.OwnerSlashRepo()) + if err != nil { + s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") + return + } + + // remove repo from db + err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) + if err != nil { + s.pages.Notice(w, "settings-delete", "Failed to update appview") + return + } + log.Println("removed repo from db") + + err = tx.Commit() + if err != nil { + log.Println("failed to commit changes", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = s.enforcer.E.SavePolicy() + if err != nil { + log.Println("failed to update ACLs", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + s.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) +} + func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { f, err := fullyResolvedRepo(r) if err != nil { diff --git a/appview/state/router.go b/appview/state/router.go index 58a57a9e..831983fd 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -153,6 +153,7 @@ func (s *State) UserRouter() http.Handler { r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { r.Get("/", s.RepoSettings) r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) + r.With(RepoPermissionMiddleware(s, "repo:delete")).Delete("/delete", s.DeleteRepo) r.Put("/branches/default", s.SetDefaultBranch) }) }) diff --git a/rbac/rbac.go b/rbac/rbac.go index 54509879..00f7a329 100644 --- a/rbac/rbac.go +++ b/rbac/rbac.go @@ -96,34 +96,62 @@ func (e *Enforcer) AddMember(domain, member string) error { return err } -func (e *Enforcer) AddRepo(member, domain, repo string) error { - // sanity check, repo must be of the form ownerDid/repo - if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { - return fmt.Errorf("invalid repo: %s", repo) - } - - _, err := e.E.AddPolicies([][]string{ +func repoPolicies(member, domain, repo string) [][]string { + return [][]string{ {member, domain, repo, "repo:settings"}, {member, domain, repo, "repo:push"}, {member, domain, repo, "repo:owner"}, {member, domain, repo, "repo:invite"}, {member, domain, repo, "repo:delete"}, {"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo - }) + } +} +func (e *Enforcer) AddRepo(member, domain, repo string) error { + err := checkRepoFormat(repo) + if err != nil { + return err + } + + _, err = e.E.AddPolicies(repoPolicies(member, domain, repo)) + return err +} +func (e *Enforcer) RemoveRepo(member, domain, repo string) error { + err := checkRepoFormat(repo) + if err != nil { + return err + } + + _, err = e.E.RemovePolicies(repoPolicies(member, domain, repo)) return err } +var ( + collaboratorPolicies = func(collaborator, domain, repo string) [][]string { + return [][]string{ + {collaborator, domain, repo, "repo:collaborator"}, + {collaborator, domain, repo, "repo:settings"}, + {collaborator, domain, repo, "repo:push"}, + } + } +) + func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error { - // sanity check, repo must be of the form ownerDid/repo - if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { - return fmt.Errorf("invalid repo: %s", repo) + err := checkRepoFormat(repo) + if err != nil { + return err } - _, err := e.E.AddPolicies([][]string{ - {collaborator, domain, repo, "repo:collaborator"}, - {collaborator, domain, repo, "repo:settings"}, - {collaborator, domain, repo, "repo:push"}, - }) + _, err = e.E.AddPolicies(collaboratorPolicies(collaborator, domain, repo)) + return err +} + +func (e *Enforcer) RemoveCollaborator(collaborator, domain, repo string) error { + err := checkRepoFormat(repo) + if err != nil { + return err + } + + _, err = e.E.RemovePolicies(collaboratorPolicies(collaborator, domain, repo)) return err } @@ -165,6 +193,10 @@ func (e *Enforcer) IsSettingsAllowed(user, domain, repo string) (bool, error) { return e.E.Enforce(user, domain, repo, "repo:settings") } +func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) { + return e.E.Enforce(user, domain, repo, "repo:invite") +} + // given a repo, what permissions does this user have? repo:owner? repo:invite? etc. func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string { var permissions []string @@ -179,10 +211,6 @@ func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string { return permissions } -func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) { - return e.E.Enforce(user, domain, repo, "repo:invite") -} - // keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin func keyMatch2Func(args ...interface{}) (interface{}, error) { name1 := args[0].(string) @@ -190,3 +218,12 @@ func keyMatch2Func(args ...interface{}) (interface{}, error) { return keyMatch2(name1, name2), nil } + +func checkRepoFormat(repo string) error { + // sanity check, repo must be of the form ownerDid/repo + if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { + return fmt.Errorf("invalid repo: %s", repo) + } + + return nil +} -- 2.51.2