Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "log/slog"
8 "net/http"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "tangled.org/core/api/tangled"
13 "tangled.org/core/knotserver/db"
14 "tangled.org/core/rbac"
15 xrpcerr "tangled.org/core/xrpc/errors"
16)
17
18const keyFetchTimeout = 15 * time.Second
19
20func (h *Xrpc) AddMember(w http.ResponseWriter, r *http.Request) {
21 l := h.Logger.With("handler", "AddMember")
22 fail := func(e xrpcerr.XrpcError, status int) {
23 l.Error("failed", "kind", e.Tag, "error", e.Message)
24 writeError(w, e, status)
25 }
26
27 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
28 if !ok {
29 fail(xrpcerr.MissingActorDidError, http.StatusForbidden)
30 return
31 }
32
33 allowed, err := h.Enforcer.IsKnotInviteAllowed(actorDid.String(), rbac.ThisServer)
34 if err != nil {
35 fail(xrpcerr.GenericError(err), http.StatusInternalServerError)
36 return
37 }
38 if !allowed {
39 fail(xrpcerr.AccessControlError(actorDid.String()), http.StatusForbidden)
40 return
41 }
42
43 var data tangled.KnotAddMember_Input
44 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
45 fail(xrpcerr.GenericError(err), http.StatusBadRequest)
46 return
47 }
48
49 subject, err := syntax.ParseDID(data.Subject)
50 if err != nil {
51 fail(xrpcerr.GenericError(err), http.StatusBadRequest)
52 return
53 }
54
55 status, xerr := h.addMemberToKnot(r.Context(), l, actorDid, subject)
56 if xerr != nil {
57 fail(*xerr, status)
58 return
59 }
60 w.WriteHeader(status)
61}
62
63func (h *Xrpc) addMemberToKnot(ctx context.Context, l *slog.Logger, addedBy, subject syntax.DID) (int, *xrpcerr.XrpcError) {
64 isOwner, err := h.Enforcer.IsKnotOwner(subject.String(), rbac.ThisServer)
65 if err != nil {
66 e := xrpcerr.GenericError(err)
67 return http.StatusInternalServerError, &e
68 }
69 if isOwner {
70 l.Info("subject is the knot owner, no-op", "subject", subject)
71 return http.StatusOK, nil
72 }
73
74 return h.applyAclGrant(ctx, l, aclGrant{
75 role: "member",
76 subject: subject,
77 inAcl: func() (bool, error) {
78 return h.Enforcer.IsKnotMember(subject.String(), rbac.ThisServer)
79 },
80 inTable: func() (bool, error) {
81 n, err := db.CountKnotMembersBySubject(h.Db, subject.String())
82 return n > 0, err
83 },
84 insertRow: func(tx *sql.Tx) error {
85 return db.AddKnotMemberDirect(tx, addedBy, subject)
86 },
87 deleteRow: func() error {
88 return db.RemoveKnotMemberDirect(h.Db, subject)
89 },
90 grantAcl: func() error {
91 _, err := h.Enforcer.TryAddKnotMember(rbac.ThisServer, subject.String())
92 return err
93 },
94 emit: func() error {
95 return h.Db.EmitKnotMemberUpdate(h.Notifier, db.AclOpAdd, subject)
96 },
97 })
98}