forked from tangled.org/core
Monorepo for Tangled

enable repo deletion in repo settings

Changed files
+188 -30
appview
db
pages
templates
state
rbac
-5
appview/db/profile.go
··· 2 3 import ( 4 "fmt" 5 - "log" 6 "sort" 7 "time" 8 ) ··· 65 return timeline, fmt.Errorf("error getting all repos by did: %w", err) 66 } 67 68 - log.Println(repos) 69 - 70 for _, repo := range repos { 71 var sourceRepo *Repo 72 - log.Println("name", repo.Name) 73 if repo.Source != "" { 74 - log.Println("source", repo.Source) 75 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 76 if err != nil { 77 return nil, err
··· 2 3 import ( 4 "fmt" 5 "sort" 6 "time" 7 ) ··· 64 return timeline, fmt.Errorf("error getting all repos by did: %w", err) 65 } 66 67 for _, repo := range repos { 68 var sourceRepo *Repo 69 if repo.Source != "" { 70 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 71 if err != nil { 72 return nil, err
+2 -2
appview/db/repos.go
··· 176 return err 177 } 178 179 - func RemoveRepo(e Execer, did, name, rkey string) error { 180 - _, err := e.Exec(`delete from repos where did = ? and name = ? and rkey = ?`, did, name, rkey) 181 return err 182 } 183
··· 176 return err 177 } 178 179 + func RemoveRepo(e Execer, did, name string) error { 180 + _, err := e.Exec(`delete from repos where did = ? and name = ?`, did, name) 181 return err 182 } 183
+8
appview/pages/pages.go
··· 246 return slices.Contains(r.Roles, "repo:settings") 247 } 248 249 func (r RolesInRepo) IsOwner() bool { 250 return slices.Contains(r.Roles, "repo:owner") 251 }
··· 246 return slices.Contains(r.Roles, "repo:settings") 247 } 248 249 + func (r RolesInRepo) CollaboratorInviteAllowed() bool { 250 + return slices.Contains(r.Roles, "repo:invite") 251 + } 252 + 253 + func (r RolesInRepo) RepoDeleteAllowed() bool { 254 + return slices.Contains(r.Roles, "repo:delete") 255 + } 256 + 257 func (r RolesInRepo) IsOwner() bool { 258 return slices.Contains(r.Roles, "repo:owner") 259 }
+14 -3
appview/pages/templates/repo/settings.html
··· 22 {{ end }} 23 </div> 24 25 - {{ if .IsCollaboratorInviteAllowed }} 26 <form hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"> 27 <label for="collaborator" class="dark:text-white" 28 >add collaborator</label ··· 45 {{ end }} 46 47 <form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="mt-6"> 48 - <label for="branch">default branch:</label> 49 - <select id="branch" name="branch" required class="p-1 border border-gray-200 bg-white"> 50 {{ range .Branches }} 51 <option 52 value="{{ . }}" ··· 61 </select> 62 <button class="btn my-2" type="text">save</button> 63 </form> 64 {{ end }}
··· 22 {{ end }} 23 </div> 24 25 + {{ if .RepoInfo.Roles.CollaboratorInviteAllowed }} 26 <form hx-put="/{{ $.RepoInfo.FullName }}/settings/collaborator"> 27 <label for="collaborator" class="dark:text-white" 28 >add collaborator</label ··· 45 {{ end }} 46 47 <form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="mt-6"> 48 + <label for="branch">default branch</label> 49 + <select id="branch" name="branch" required class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"> 50 {{ range .Branches }} 51 <option 52 value="{{ . }}" ··· 61 </select> 62 <button class="btn my-2" type="text">save</button> 63 </form> 64 + 65 + {{ if .RepoInfo.Roles.RepoDeleteAllowed }} 66 + <form hx-confirm="Are you sure you want to delete this repository?" hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete" class="mt-6"> 67 + <label for="branch">delete repository</label> 68 + <button class="btn my-2" type="text">delete</button> 69 + <span> 70 + Deleting a repository is irreversible and permanent. 71 + </span> 72 + </form> 73 + {{ end }} 74 + 75 {{ end }}
+106
appview/state/repo.go
··· 596 597 } 598 599 func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 600 f, err := fullyResolvedRepo(r) 601 if err != nil {
··· 596 597 } 598 599 + func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 600 + user := s.auth.GetUser(r) 601 + 602 + f, err := fullyResolvedRepo(r) 603 + if err != nil { 604 + log.Println("failed to get repo and knot", err) 605 + return 606 + } 607 + 608 + // remove record from pds 609 + xrpcClient, _ := s.auth.AuthorizedClient(r) 610 + repoRkey := f.RepoAt.RecordKey().String() 611 + _, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{ 612 + Collection: tangled.RepoNSID, 613 + Repo: user.Did, 614 + Rkey: repoRkey, 615 + }) 616 + if err != nil { 617 + log.Printf("failed to delete record: %s", err) 618 + s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 619 + return 620 + } 621 + log.Println("removed repo record ", f.RepoAt.String()) 622 + 623 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 624 + if err != nil { 625 + log.Printf("no key found for domain %s: %s\n", f.Knot, err) 626 + return 627 + } 628 + 629 + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 630 + if err != nil { 631 + log.Println("failed to create client to ", f.Knot) 632 + return 633 + } 634 + 635 + ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName) 636 + if err != nil { 637 + log.Printf("failed to make request to %s: %s", f.Knot, err) 638 + return 639 + } 640 + 641 + if ksResp.StatusCode != http.StatusNoContent { 642 + log.Println("failed to remove repo from knot, continuing anyway ", f.Knot) 643 + } else { 644 + log.Println("removed repo from knot ", f.Knot) 645 + } 646 + 647 + tx, err := s.db.BeginTx(r.Context(), nil) 648 + if err != nil { 649 + log.Println("failed to start tx") 650 + w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 651 + return 652 + } 653 + defer func() { 654 + tx.Rollback() 655 + err = s.enforcer.E.LoadPolicy() 656 + if err != nil { 657 + log.Println("failed to rollback policies") 658 + } 659 + }() 660 + 661 + // remove collaborator RBAC 662 + repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot) 663 + if err != nil { 664 + s.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 665 + return 666 + } 667 + for _, c := range repoCollaborators { 668 + did := c[0] 669 + s.enforcer.RemoveCollaborator(did, f.Knot, f.OwnerSlashRepo()) 670 + } 671 + log.Println("removed collaborators") 672 + 673 + // remove repo RBAC 674 + err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.OwnerSlashRepo()) 675 + if err != nil { 676 + s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 677 + return 678 + } 679 + 680 + // remove repo from db 681 + err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 682 + if err != nil { 683 + s.pages.Notice(w, "settings-delete", "Failed to update appview") 684 + return 685 + } 686 + log.Println("removed repo from db") 687 + 688 + err = tx.Commit() 689 + if err != nil { 690 + log.Println("failed to commit changes", err) 691 + http.Error(w, err.Error(), http.StatusInternalServerError) 692 + return 693 + } 694 + 695 + err = s.enforcer.E.SavePolicy() 696 + if err != nil { 697 + log.Println("failed to update ACLs", err) 698 + http.Error(w, err.Error(), http.StatusInternalServerError) 699 + return 700 + } 701 + 702 + s.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 703 + } 704 + 705 func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 706 f, err := fullyResolvedRepo(r) 707 if err != nil {
+1
appview/state/router.go
··· 153 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 154 r.Get("/", s.RepoSettings) 155 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 156 r.Put("/branches/default", s.SetDefaultBranch) 157 }) 158 })
··· 153 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 154 r.Get("/", s.RepoSettings) 155 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 156 + r.With(RepoPermissionMiddleware(s, "repo:delete")).Delete("/delete", s.DeleteRepo) 157 r.Put("/branches/default", s.SetDefaultBranch) 158 }) 159 })
+57 -20
rbac/rbac.go
··· 96 return err 97 } 98 99 - func (e *Enforcer) AddRepo(member, domain, repo string) error { 100 - // sanity check, repo must be of the form ownerDid/repo 101 - if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { 102 - return fmt.Errorf("invalid repo: %s", repo) 103 - } 104 - 105 - _, err := e.E.AddPolicies([][]string{ 106 {member, domain, repo, "repo:settings"}, 107 {member, domain, repo, "repo:push"}, 108 {member, domain, repo, "repo:owner"}, 109 {member, domain, repo, "repo:invite"}, 110 {member, domain, repo, "repo:delete"}, 111 {"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo 112 - }) 113 return err 114 } 115 116 func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error { 117 - // sanity check, repo must be of the form ownerDid/repo 118 - if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { 119 - return fmt.Errorf("invalid repo: %s", repo) 120 } 121 122 - _, err := e.E.AddPolicies([][]string{ 123 - {collaborator, domain, repo, "repo:collaborator"}, 124 - {collaborator, domain, repo, "repo:settings"}, 125 - {collaborator, domain, repo, "repo:push"}, 126 - }) 127 return err 128 } 129 ··· 165 return e.E.Enforce(user, domain, repo, "repo:settings") 166 } 167 168 // given a repo, what permissions does this user have? repo:owner? repo:invite? etc. 169 func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string { 170 var permissions []string ··· 179 return permissions 180 } 181 182 - func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) { 183 - return e.E.Enforce(user, domain, repo, "repo:invite") 184 - } 185 - 186 // keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin 187 func keyMatch2Func(args ...interface{}) (interface{}, error) { 188 name1 := args[0].(string) ··· 190 191 return keyMatch2(name1, name2), nil 192 }
··· 96 return err 97 } 98 99 + func repoPolicies(member, domain, repo string) [][]string { 100 + return [][]string{ 101 {member, domain, repo, "repo:settings"}, 102 {member, domain, repo, "repo:push"}, 103 {member, domain, repo, "repo:owner"}, 104 {member, domain, repo, "repo:invite"}, 105 {member, domain, repo, "repo:delete"}, 106 {"server:owner", domain, repo, "repo:delete"}, // server owner can delete any repo 107 + } 108 + } 109 + func (e *Enforcer) AddRepo(member, domain, repo string) error { 110 + err := checkRepoFormat(repo) 111 + if err != nil { 112 + return err 113 + } 114 + 115 + _, err = e.E.AddPolicies(repoPolicies(member, domain, repo)) 116 + return err 117 + } 118 + func (e *Enforcer) RemoveRepo(member, domain, repo string) error { 119 + err := checkRepoFormat(repo) 120 + if err != nil { 121 + return err 122 + } 123 + 124 + _, err = e.E.RemovePolicies(repoPolicies(member, domain, repo)) 125 return err 126 } 127 128 + var ( 129 + collaboratorPolicies = func(collaborator, domain, repo string) [][]string { 130 + return [][]string{ 131 + {collaborator, domain, repo, "repo:collaborator"}, 132 + {collaborator, domain, repo, "repo:settings"}, 133 + {collaborator, domain, repo, "repo:push"}, 134 + } 135 + } 136 + ) 137 + 138 func (e *Enforcer) AddCollaborator(collaborator, domain, repo string) error { 139 + err := checkRepoFormat(repo) 140 + if err != nil { 141 + return err 142 } 143 144 + _, err = e.E.AddPolicies(collaboratorPolicies(collaborator, domain, repo)) 145 + return err 146 + } 147 + 148 + func (e *Enforcer) RemoveCollaborator(collaborator, domain, repo string) error { 149 + err := checkRepoFormat(repo) 150 + if err != nil { 151 + return err 152 + } 153 + 154 + _, err = e.E.RemovePolicies(collaboratorPolicies(collaborator, domain, repo)) 155 return err 156 } 157 ··· 193 return e.E.Enforce(user, domain, repo, "repo:settings") 194 } 195 196 + func (e *Enforcer) IsCollaboratorInviteAllowed(user, domain, repo string) (bool, error) { 197 + return e.E.Enforce(user, domain, repo, "repo:invite") 198 + } 199 + 200 // given a repo, what permissions does this user have? repo:owner? repo:invite? etc. 201 func (e *Enforcer) GetPermissionsInRepo(user, domain, repo string) []string { 202 var permissions []string ··· 211 return permissions 212 } 213 214 // keyMatch2Func is a wrapper for keyMatch2 to make it compatible with Casbin 215 func keyMatch2Func(args ...interface{}) (interface{}, error) { 216 name1 := args[0].(string) ··· 218 219 return keyMatch2(name1, name2), nil 220 } 221 + 222 + func checkRepoFormat(repo string) error { 223 + // sanity check, repo must be of the form ownerDid/repo 224 + if parts := strings.SplitN(repo, "/", 2); !strings.HasPrefix(parts[0], "did:") { 225 + return fmt.Errorf("invalid repo: %s", repo) 226 + } 227 + 228 + return nil 229 + }