Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
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
15type collabTarget struct {
16 repoDid syntax.DID
17 subject syntax.DID
18 ownerNoop bool
19}
20
21func (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
59func (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}