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

2 rounds 4 comments
sign up or login to add to the discussion
1 commit
expand
feat(repo): add collaborator removal
no conflicts, ready to merge
expand 1 comment

added collaborators table to keep track in knotserver and spindle

murex.tngl.sh submitted #0
1 commit
expand
feat(repo): add collaborator removal
expand 3 comments

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
  • nice catch about removing keys if no other relationship exist, will dig into this

lovely, thanks for picking this up. the tricky bit is that knots presently do not hold rkeys in their DBs, and embedded tap would help us backfill and achieve this easily.