Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

knotserver/xrpc: add/remove repo collaborators directly

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (Jun 16, 2026, 9:04 PM +0300) commit 5863a312 parent 4fce2f5d change-id mzpwtkty
+197
+120
knotserver/xrpc/add_collaborator.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "database/sql" 5 + "encoding/json" 6 + "net/http" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "tangled.org/core/api/tangled" 10 + "tangled.org/core/knotserver/db" 11 + "tangled.org/core/rbac" 12 + xrpcerr "tangled.org/core/xrpc/errors" 13 + ) 14 + 15 + type collabTarget struct { 16 + repoDid syntax.DID 17 + subject syntax.DID 18 + ownerNoop bool 19 + } 20 + 21 + func (h *Xrpc) resolveCollabTarget(actor syntax.DID, repo, subject string) (collabTarget, int, *xrpcerr.XrpcError) { 22 + fail := func(status int, e xrpcerr.XrpcError) (collabTarget, int, *xrpcerr.XrpcError) { 23 + return collabTarget{}, status, &e 24 + } 25 + 26 + repoDid, err := syntax.ParseDID(repo) 27 + if err != nil { 28 + return fail(http.StatusBadRequest, xrpcerr.InvalidRepoError(repo)) 29 + } 30 + subjectDid, err := syntax.ParseDID(subject) 31 + if err != nil { 32 + return fail(http.StatusBadRequest, xrpcerr.GenericError(err)) 33 + } 34 + 35 + exists, err := h.Db.RepoDidExists(repoDid.String()) 36 + if err != nil { 37 + return fail(http.StatusInternalServerError, xrpcerr.GenericError(err)) 38 + } 39 + if !exists { 40 + return fail(http.StatusNotFound, xrpcerr.RepoNotFoundError) 41 + } 42 + 43 + allowed, err := h.Enforcer.IsCollaboratorInviteAllowed(actor.String(), rbac.ThisServer, repoDid.String()) 44 + if err != nil { 45 + return fail(http.StatusInternalServerError, xrpcerr.GenericError(err)) 46 + } 47 + if !allowed { 48 + return fail(http.StatusForbidden, xrpcerr.AccessControlError(actor.String())) 49 + } 50 + 51 + isOwner, err := h.Enforcer.IsRepoOwner(subjectDid.String(), rbac.ThisServer, repoDid.String()) 52 + if err != nil { 53 + return fail(http.StatusInternalServerError, xrpcerr.GenericError(err)) 54 + } 55 + 56 + return collabTarget{repoDid: repoDid, subject: subjectDid, ownerNoop: isOwner}, 0, nil 57 + } 58 + 59 + func (h *Xrpc) AddCollaborator(w http.ResponseWriter, r *http.Request) { 60 + l := h.Logger.With("handler", "AddCollaborator") 61 + fail := func(e xrpcerr.XrpcError, status int) { 62 + l.Error("failed", "kind", e.Tag, "error", e.Message) 63 + writeError(w, e, status) 64 + } 65 + 66 + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 67 + if !ok { 68 + fail(xrpcerr.MissingActorDidError, http.StatusForbidden) 69 + return 70 + } 71 + 72 + var data tangled.RepoAddCollaborator_Input 73 + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 74 + fail(xrpcerr.GenericError(err), http.StatusBadRequest) 75 + return 76 + } 77 + 78 + t, status, xerr := h.resolveCollabTarget(actorDid, data.Repo, data.Subject) 79 + if xerr != nil { 80 + fail(*xerr, status) 81 + return 82 + } 83 + if t.ownerNoop { 84 + l.Info("subject is the repo owner, no-op", "repoDid", t.repoDid, "subject", t.subject) 85 + w.WriteHeader(http.StatusOK) 86 + return 87 + } 88 + 89 + status, xerr = h.applyAclGrant(r.Context(), l.With("repoDid", t.repoDid), aclGrant{ 90 + role: "collaborator", 91 + subject: t.subject, 92 + inAcl: func() (bool, error) { 93 + return h.Enforcer.IsRepoCollaborator(t.subject.String(), rbac.ThisServer, t.repoDid.String()) 94 + }, 95 + inTable: func() (bool, error) { 96 + return db.IsCollaborator(h.Db, t.repoDid, t.subject) 97 + }, 98 + insertRow: func(tx *sql.Tx) error { 99 + return db.AddCollaborator(tx, db.Collaborator{ 100 + RepoDid: t.repoDid, 101 + Subject: t.subject, 102 + AddedBy: actorDid, 103 + }) 104 + }, 105 + deleteRow: func() error { 106 + return db.RemoveCollaborator(h.Db, t.repoDid, t.subject) 107 + }, 108 + grantAcl: func() error { 109 + return h.Enforcer.AddCollaborator(t.subject.String(), rbac.ThisServer, t.repoDid.String()) 110 + }, 111 + emit: func() error { 112 + return h.Db.EmitCollaboratorUpdate(h.Notifier, db.AclOpAdd, t.subject, t.repoDid) 113 + }, 114 + }) 115 + if xerr != nil { 116 + fail(*xerr, status) 117 + return 118 + } 119 + w.WriteHeader(status) 120 + }
+75
knotserver/xrpc/remove_collaborator.go
··· 1 + package xrpc 2 + 3 + import ( 4 + "database/sql" 5 + "encoding/json" 6 + "net/http" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "tangled.org/core/api/tangled" 10 + "tangled.org/core/knotserver/db" 11 + "tangled.org/core/rbac" 12 + xrpcerr "tangled.org/core/xrpc/errors" 13 + ) 14 + 15 + func (h *Xrpc) RemoveCollaborator(w http.ResponseWriter, r *http.Request) { 16 + l := h.Logger.With("handler", "RemoveCollaborator") 17 + fail := func(e xrpcerr.XrpcError, status int) { 18 + l.Error("failed", "kind", e.Tag, "error", e.Message) 19 + writeError(w, e, status) 20 + } 21 + 22 + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 23 + if !ok { 24 + fail(xrpcerr.MissingActorDidError, http.StatusForbidden) 25 + return 26 + } 27 + 28 + var data tangled.RepoRemoveCollaborator_Input 29 + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 30 + fail(xrpcerr.GenericError(err), http.StatusBadRequest) 31 + return 32 + } 33 + 34 + t, status, xerr := h.resolveCollabTarget(actorDid, data.Repo, data.Subject) 35 + if xerr != nil { 36 + fail(*xerr, status) 37 + return 38 + } 39 + if t.ownerNoop { 40 + l.Info("subject is the repo owner, no-op", "repoDid", t.repoDid, "subject", t.subject) 41 + w.WriteHeader(http.StatusOK) 42 + return 43 + } 44 + 45 + status, xerr = h.applyAclRevoke(r.Context(), l.With("repoDid", t.repoDid), aclRevoke{ 46 + role: "collaborator", 47 + subject: t.subject, 48 + inAcl: func() (bool, error) { 49 + return h.Enforcer.IsRepoCollaborator(t.subject.String(), rbac.ThisServer, t.repoDid.String()) 50 + }, 51 + inTable: func() (bool, error) { 52 + return db.IsCollaborator(h.Db, t.repoDid, t.subject) 53 + }, 54 + removeAcl: func() (bool, error) { 55 + if err := h.Enforcer.RemoveCollaborator(t.subject.String(), rbac.ThisServer, t.repoDid.String()); err != nil { 56 + return false, err 57 + } 58 + return true, nil 59 + }, 60 + restoreAcl: func() error { 61 + return h.Enforcer.AddCollaborator(t.subject.String(), rbac.ThisServer, t.repoDid.String()) 62 + }, 63 + deleteRow: func(tx *sql.Tx) error { 64 + return db.RemoveCollaborator(tx, t.repoDid, t.subject) 65 + }, 66 + emit: func() error { 67 + return h.Db.EmitCollaboratorUpdate(h.Notifier, db.AclOpRemove, t.subject, t.repoDid) 68 + }, 69 + }) 70 + if xerr != nil { 71 + fail(*xerr, status) 72 + return 73 + } 74 + w.WriteHeader(status) 75 + }
+2
knotserver/xrpc/xrpc.go
··· 58 58 r.Post("/"+tangled.RepoMergeNSID, x.Merge) 59 59 r.Post("/"+tangled.KnotAddMemberNSID, x.AddMember) 60 60 r.Post("/"+tangled.KnotRemoveMemberNSID, x.RemoveMember) 61 + r.Post("/"+tangled.RepoAddCollaboratorNSID, x.AddCollaborator) 62 + r.Post("/"+tangled.RepoRemoveCollaboratorNSID, x.RemoveCollaborator) 61 63 }) 62 64 63 65 // merge check is an open endpoint