Monorepo for Tangled tangled.org

feat(repo): add collaborator removal #1123

open opened by murex.tngl.sh targeting master from murex.tngl.sh/tangled: feat/remove-collaborator
  • Add RemoveCollaborator handler: delete PDS record, RBAC policy, and DB row
  • Add DELETE /settings/collaborator route (repo:owner)
  • Load collaborator rkey from DB for removal
  • Add trash icon remove button on access settings

close: https://tangled.org/tangled.org/core/issues/210

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:owyua2lvxbs55wyhs22dqu2s/sh.tangled.repo.pull/3mgjrpxldea22
+127 -8
Diff #0
+3 -2
appview/pages/pages.go
··· 902 902 } 903 903 904 904 type Collaborator struct { 905 - Did string 906 - Role string 905 + Did string 906 + Role string 907 + Rkey string // set for collaborators (not owner) so remove button can delete the PDS record 907 908 } 908 909 909 910 type RepoSettingsParams struct {
+19
appview/pages/templates/repo/settings/access.html
··· 13 13 14 14 {{ define "collaboratorSettings" }} 15 15 <div class="grid grid-cols-1 gap-4 items-center"> 16 + <div id="remove-collaborator-error" class="text-red-500 dark:text-red-400"></div> 16 17 <div class="col-span-1"> 17 18 <h2 class="text-sm pb-2 uppercase font-bold">Collaborators</h2> 18 19 <p class="text-gray-500 dark:text-gray-400"> ··· 40 41 </a> 41 42 <p class="text-sm text-gray-500 dark:text-gray-400">{{ .Role }}</p> 42 43 </div> 44 + {{ if and (eq .Role "collaborator") .Rkey $.RepoInfo.Roles.IsOwner }} 45 + <form 46 + method="post" 47 + action="/{{ $.RepoInfo.FullName }}/settings/collaborator?subject_did={{ .Did }}" 48 + hx-delete="/{{ $.RepoInfo.FullName }}/settings/collaborator?subject_did={{ .Did }}" 49 + hx-swap="none" 50 + hx-confirm="Remove {{ $handle }} from collaborators?" 51 + class="inline" 52 + > 53 + <button 54 + type="submit" 55 + class="btn-ghost p-1 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300" 56 + title="Remove collaborator" 57 + > 58 + {{ i "trash-2" "size-4" }} 59 + </button> 60 + </form> 61 + {{ end }} 43 62 </div> 44 63 </div> 45 64 {{ end }}
+95
appview/repo/repo.go
··· 821 821 rp.pages.HxRefresh(w) 822 822 } 823 823 824 + func (rp *Repo) RemoveCollaborator(w http.ResponseWriter, r *http.Request) { 825 + user := rp.oauth.GetMultiAccountUser(r) 826 + l := rp.logger.With("handler", "RemoveCollaborator") 827 + l = l.With("did", user.Active.Did) 828 + 829 + f, err := rp.repoResolver.Resolve(r) 830 + if err != nil { 831 + l.Error("failed to get repo and knot", "err", err) 832 + return 833 + } 834 + 835 + errorId := "remove-collaborator-error" 836 + fail := func(msg string, err error) { 837 + l.Error(msg, "err", err) 838 + rp.pages.Notice(w, errorId, msg) 839 + } 840 + 841 + subjectDid := r.FormValue("subject_did") 842 + if subjectDid == "" { 843 + fail("Invalid form.", nil) 844 + return 845 + } 846 + 847 + if subjectDid == user.Active.Did { 848 + fail("You cannot remove yourself as owner.", nil) 849 + return 850 + } 851 + 852 + // look up collaborator in db to get rkey (record lives on owner's PDS) 853 + collabs, err := db.GetCollaborators(rp.db, orm.FilterEq("repo_at", f.RepoAt()), orm.FilterEq("subject_did", subjectDid)) 854 + if err != nil { 855 + fail("Failed to remove collaborator.", err) 856 + return 857 + } 858 + if len(collabs) == 0 { 859 + fail("Collaborator not found.", nil) 860 + return 861 + } 862 + collab := collabs[0] 863 + 864 + client, err := rp.oauth.AuthorizedClient(r) 865 + if err != nil { 866 + fail("Failed to write to PDS.", err) 867 + return 868 + } 869 + 870 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 871 + Collection: tangled.RepoCollaboratorNSID, 872 + Repo: f.Did, 873 + Rkey: collab.Rkey, 874 + }) 875 + if err != nil { 876 + fail("Failed to remove collaborator record from PDS.", err) 877 + return 878 + } 879 + 880 + tx, err := rp.db.BeginTx(r.Context(), nil) 881 + if err != nil { 882 + fail("Failed to remove collaborator.", err) 883 + return 884 + } 885 + 886 + rollback := func() { 887 + tx.Rollback() 888 + rp.enforcer.E.LoadPolicy() 889 + } 890 + defer rollback() 891 + 892 + err = rp.enforcer.RemoveCollaborator(subjectDid, f.Knot, f.DidSlashRepo()) 893 + if err != nil { 894 + fail("Failed to remove collaborator permissions.", err) 895 + return 896 + } 897 + 898 + err = db.DeleteCollaborator(tx, orm.FilterEq("repo_at", f.RepoAt()), orm.FilterEq("subject_did", subjectDid)) 899 + if err != nil { 900 + fail("Failed to remove collaborator.", err) 901 + return 902 + } 903 + 904 + err = tx.Commit() 905 + if err != nil { 906 + fail("Failed to remove collaborator.", err) 907 + return 908 + } 909 + 910 + err = rp.enforcer.E.SavePolicy() 911 + if err != nil { 912 + fail("Failed to update permissions.", err) 913 + return 914 + } 915 + 916 + rp.pages.HxRefresh(w) 917 + } 918 + 824 919 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 825 920 user := rp.oauth.GetMultiAccountUser(r) 826 921 l := rp.logger.With("handler", "DeleteRepo")
+1
appview/repo/router.go
··· 83 83 r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/label/subscribe", rp.SubscribeLabel) 84 84 r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/label/unsubscribe", rp.UnsubscribeLabel) 85 85 r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator) 86 + r.With(mw.RepoPermissionMiddleware("repo:owner")).Delete("/collaborator", rp.RemoveCollaborator) 86 87 r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo) 87 88 r.Put("/branches/default", rp.SetDefaultBranch) 88 89 r.Put("/secrets", rp.Secrets)
+9 -6
appview/repo/settings.go
··· 264 264 if err != nil { 265 265 return nil, err 266 266 } 267 + // load db collaborators for rkey (needed to remove via PDS delete) 268 + dbCollabs, _ := db.GetCollaborators(rp.db, orm.FilterEq("repo_at", repo.RepoAt())) 269 + rkeyByDid := make(map[string]string) 270 + for _, c := range dbCollabs { 271 + rkeyByDid[c.SubjectDid.String()] = c.Rkey 272 + } 267 273 var collaborators []pages.Collaborator 268 274 for _, item := range repoCollaborators { 269 - // currently only two roles: owner and member 270 275 var role string 271 276 switch item[3] { 272 277 case "repo:owner": ··· 276 281 default: 277 282 continue 278 283 } 279 - 280 284 did := item[0] 281 - 282 - c := pages.Collaborator{ 283 - Did: did, 284 - Role: role, 285 + c := pages.Collaborator{Did: did, Role: role} 286 + if role == "collaborator" { 287 + c.Rkey = rkeyByDid[did] 285 288 } 286 289 collaborators = append(collaborators, c) 287 290 }

History

1 round 1 comment
sign up or login to add to the discussion
murex.tngl.sh submitted #0
1 commit
expand
feat(repo): add collaborator removal
no conflicts, ready to merge
expand 1 comment

thanks for the contribution! this PR does not handle all the scenarios:

  • the collaborator should be removed from the knot
  • their key must be removed from the knot if they don't have any other collaborator/membership relationship with the knot
  • likewise with spindles