+11
-11
spindle/config/config.go
+11
-11
spindle/config/config.go
···
9
9
)
10
10
11
11
type Server struct {
12
-
ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"`
13
-
DBPath string `env:"DB_PATH, default=spindle.db"`
14
-
Hostname string `env:"HOSTNAME, required"`
15
-
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
16
-
PlcUrl string `env:"PLC_URL, default=https://plc.directory"`
17
-
Dev bool `env:"DEV, default=false"`
18
-
Owner string `env:"OWNER, required"`
19
-
Secrets Secrets `env:",prefix=SECRETS_"`
20
-
LogDir string `env:"LOG_DIR, default=/var/log/spindle"`
21
-
QueueSize int `env:"QUEUE_SIZE, default=100"`
22
-
MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time
12
+
ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"`
13
+
DBPath string `env:"DB_PATH, default=spindle.db"`
14
+
Hostname string `env:"HOSTNAME, required"`
15
+
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
16
+
PlcUrl string `env:"PLC_URL, default=https://plc.directory"`
17
+
Dev bool `env:"DEV, default=false"`
18
+
Owner syntax.DID `env:"OWNER, required"`
19
+
Secrets Secrets `env:",prefix=SECRETS_"`
20
+
LogDir string `env:"LOG_DIR, default=/var/log/spindle"`
21
+
QueueSize int `env:"QUEUE_SIZE, default=100"`
22
+
MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time
23
23
}
24
24
25
25
func (s Server) Did() syntax.DID {
+17
-41
spindle/ingester.go
+17
-41
spindle/ingester.go
···
9
9
10
10
"tangled.org/core/api/tangled"
11
11
"tangled.org/core/eventconsumer"
12
-
"tangled.org/core/rbac"
13
12
"tangled.org/core/spindle/db"
14
13
15
14
comatproto "github.com/bluesky-social/indigo/api/atproto"
16
-
"github.com/bluesky-social/indigo/atproto/identity"
17
15
"github.com/bluesky-social/indigo/atproto/syntax"
18
16
"github.com/bluesky-social/indigo/xrpc"
19
17
"github.com/bluesky-social/jetstream/pkg/models"
20
-
securejoin "github.com/cyphar/filepath-securejoin"
21
18
)
22
19
23
20
type Ingester func(ctx context.Context, e *models.Event) error
···
79
76
return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain)
80
77
}
81
78
82
-
ok, err := s.e.IsSpindleInviteAllowed(did, rbacDomain)
79
+
ok, err := s.e.IsSpindleMemberInviteAllowed(syntax.DID(did), s.cfg.Server.Did())
83
80
if err != nil || !ok {
84
81
l.Error("failed to add member", "did", did, "error", err)
85
82
return fmt.Errorf("failed to enforce permissions: %w", err)
···
96
93
return fmt.Errorf("failed to add member: %w", err)
97
94
}
98
95
99
-
if err := s.e.AddSpindleMember(rbacDomain, record.Subject); err != nil {
96
+
if err := s.e.AddSpindleMember(syntax.DID(record.Subject), s.cfg.Server.Did()); err != nil {
100
97
l.Error("failed to add member", "error", err)
101
98
return fmt.Errorf("failed to add member: %w", err)
102
99
}
···
122
119
return fmt.Errorf("failed to remove member: %w", err)
123
120
}
124
121
125
-
if err := s.e.RemoveSpindleMember(rbacDomain, record.Subject.String()); err != nil {
122
+
if err := s.e.RemoveSpindleMember(record.Subject, s.cfg.Server.Did()); err != nil {
126
123
l.Error("failed to add member", "error", err)
127
124
return fmt.Errorf("failed to add member: %w", err)
128
125
}
···
176
173
return fmt.Errorf("failed to add repo: %w", err)
177
174
}
178
175
179
-
didSlashRepo, err := securejoin.SecureJoin(did, record.Name)
180
-
if err != nil {
181
-
return err
182
-
}
176
+
repoAt := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, e.Commit.Collection, e.Commit.RKey))
183
177
184
178
// add repo to rbac
185
-
if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil {
179
+
if err := s.e.AddRepo(repoAt); err != nil {
186
180
l.Error("failed to add repo to enforcer", "error", err)
187
181
return fmt.Errorf("failed to add repo: %w", err)
188
182
}
189
183
190
184
// add collaborators to rbac
191
-
owner, err := s.res.ResolveIdent(ctx, did)
192
-
if err != nil || owner.Handle.IsInvalidHandle() {
193
-
return err
194
-
}
195
-
if err := s.fetchAndAddCollaborators(ctx, owner, didSlashRepo); err != nil {
185
+
if err := s.fetchAndAddCollaborators(ctx, repoAt); err != nil {
196
186
return err
197
187
}
198
188
···
234
224
return nil
235
225
}
236
226
237
-
// TODO: get rid of this entirely
238
-
// resolve this aturi to extract the repo record
239
-
owner, err := s.res.ResolveIdent(ctx, repoAt.Authority().String())
240
-
if err != nil || owner.Handle.IsInvalidHandle() {
241
-
return fmt.Errorf("failed to resolve handle: %w", err)
242
-
}
243
-
244
-
xrpcc := xrpc.Client{
245
-
Host: owner.PDSEndpoint(),
246
-
}
247
-
248
-
resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
249
-
if err != nil {
250
-
return err
251
-
}
252
-
253
-
repo := resp.Value.Val.(*tangled.Repo)
254
-
didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name)
255
-
256
227
// check perms for this user
257
-
if ok, err := s.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil {
228
+
if ok, err := s.e.IsRepoCollaboratorInviteAllowed(syntax.DID(e.Did), repoAt); !ok || err != nil {
258
229
return fmt.Errorf("insufficient permissions: %w", err)
259
230
}
260
231
261
232
// add collaborator to rbac
262
-
if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil {
233
+
if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), repoAt); err != nil {
263
234
l.Error("failed to add repo to enforcer", "error", err)
264
235
return fmt.Errorf("failed to add repo: %w", err)
265
236
}
···
269
240
return nil
270
241
}
271
242
272
-
func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error {
243
+
func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, repo syntax.ATURI) error {
273
244
l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators")
274
245
275
246
l.Info("fetching and adding existing collaborators")
276
247
248
+
ident, err := s.res.ResolveIdent(ctx, repo.Authority().String())
249
+
if err != nil || ident.Handle.IsInvalidHandle() {
250
+
return fmt.Errorf("failed to resolve handle: %w", err)
251
+
}
252
+
277
253
xrpcc := xrpc.Client{
278
-
Host: owner.PDSEndpoint(),
254
+
Host: ident.PDSEndpoint(),
279
255
}
280
256
281
-
resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, owner.DID.String(), false)
257
+
resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, ident.DID.String(), false)
282
258
if err != nil {
283
259
return err
284
260
}
···
290
266
}
291
267
record := r.Value.Val.(*tangled.RepoCollaborator)
292
268
293
-
if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil {
269
+
if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)); err != nil {
294
270
l.Error("failed to add repo to enforcer", "error", err)
295
271
errors.Join(errs, fmt.Errorf("failed to add repo: %w", err))
296
272
}
+6
-47
spindle/server.go
+6
-47
spindle/server.go
···
17
17
"tangled.org/core/jetstream"
18
18
"tangled.org/core/log"
19
19
"tangled.org/core/notifier"
20
-
"tangled.org/core/rbac"
20
+
"tangled.org/core/rbac2"
21
21
"tangled.org/core/spindle/config"
22
22
"tangled.org/core/spindle/db"
23
23
"tangled.org/core/spindle/engine"
···
32
32
//go:embed motd
33
33
var motd []byte
34
34
35
-
const (
36
-
rbacDomain = "thisserver"
37
-
)
38
-
39
35
type Spindle struct {
40
36
jc *jetstream.JetstreamClient
41
37
db *db.DB
42
-
e *rbac.Enforcer
38
+
e *rbac2.Enforcer
43
39
l *slog.Logger
44
40
n *notifier.Notifier
45
41
engs map[string]models.Engine
···
59
55
return nil, fmt.Errorf("failed to setup db: %w", err)
60
56
}
61
57
62
-
e, err := rbac.NewEnforcer(cfg.Server.DBPath)
58
+
e, err := rbac2.NewEnforcer(cfg.Server.DBPath)
63
59
if err != nil {
64
60
return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err)
65
61
}
66
-
e.E.EnableAutoSave(true)
67
62
68
63
n := notifier.New()
69
64
···
104
99
if err != nil {
105
100
return nil, fmt.Errorf("failed to setup jetstream client: %w", err)
106
101
}
107
-
jc.AddDid(cfg.Server.Owner)
102
+
jc.AddDid(cfg.Server.Owner.String())
108
103
109
104
// Check if the spindle knows about any Dids;
110
105
dids, err := d.GetAllDids()
···
130
125
vault: vault,
131
126
}
132
127
133
-
err = e.AddSpindle(rbacDomain)
134
-
if err != nil {
135
-
return nil, fmt.Errorf("failed to set rbac domain: %w", err)
136
-
}
137
-
err = spindle.configureOwner()
128
+
err = e.SetSpindleOwner(spindle.cfg.Server.Owner, spindle.cfg.Server.Did())
138
129
if err != nil {
139
130
return nil, err
140
131
}
···
197
188
}
198
189
199
190
// Enforcer returns the RBAC enforcer instance.
200
-
func (s *Spindle) Enforcer() *rbac.Enforcer {
191
+
func (s *Spindle) Enforcer() *rbac2.Enforcer {
201
192
return s.e
202
193
}
203
194
···
382
373
383
374
return nil
384
375
}
385
-
386
-
func (s *Spindle) configureOwner() error {
387
-
cfgOwner := s.cfg.Server.Owner
388
-
389
-
existing, err := s.e.GetSpindleUsersByRole("server:owner", rbacDomain)
390
-
if err != nil {
391
-
return err
392
-
}
393
-
394
-
switch len(existing) {
395
-
case 0:
396
-
// no owner configured, continue
397
-
case 1:
398
-
// find existing owner
399
-
existingOwner := existing[0]
400
-
401
-
// no ownership change, this is okay
402
-
if existingOwner == s.cfg.Server.Owner {
403
-
break
404
-
}
405
-
406
-
// remove existing owner
407
-
err = s.e.RemoveSpindleOwner(rbacDomain, existingOwner)
408
-
if err != nil {
409
-
return nil
410
-
}
411
-
default:
412
-
return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", s.cfg.Server.DBPath)
413
-
}
414
-
415
-
return s.e.AddSpindleOwner(rbacDomain, cfgOwner)
416
-
}
+1
-2
spindle/xrpc/add_secret.go
+1
-2
spindle/xrpc/add_secret.go
···
11
11
"github.com/bluesky-social/indigo/xrpc"
12
12
securejoin "github.com/cyphar/filepath-securejoin"
13
13
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
14
"tangled.org/core/spindle/secrets"
16
15
xrpcerr "tangled.org/core/xrpc/errors"
17
16
)
···
68
67
return
69
68
}
70
69
71
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
70
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
72
71
l.Error("insufficent permissions", "did", actorDid.String())
73
72
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
74
73
return
+1
-2
spindle/xrpc/list_secrets.go
+1
-2
spindle/xrpc/list_secrets.go
···
11
11
"github.com/bluesky-social/indigo/xrpc"
12
12
securejoin "github.com/cyphar/filepath-securejoin"
13
13
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
14
"tangled.org/core/spindle/secrets"
16
15
xrpcerr "tangled.org/core/xrpc/errors"
17
16
)
···
63
62
return
64
63
}
65
64
66
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
65
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
67
66
l.Error("insufficent permissions", "did", actorDid.String())
68
67
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
69
68
return
+1
-1
spindle/xrpc/owner.go
+1
-1
spindle/xrpc/owner.go
+1
-26
spindle/xrpc/pipeline_cancelPipeline.go
+1
-26
spindle/xrpc/pipeline_cancelPipeline.go
···
6
6
"net/http"
7
7
"strings"
8
8
9
-
"github.com/bluesky-social/indigo/api/atproto"
10
9
"github.com/bluesky-social/indigo/atproto/syntax"
11
-
"github.com/bluesky-social/indigo/xrpc"
12
-
securejoin "github.com/cyphar/filepath-securejoin"
13
10
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
11
"tangled.org/core/spindle/models"
16
12
xrpcerr "tangled.org/core/xrpc/errors"
17
13
)
···
53
49
return
54
50
}
55
51
56
-
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
57
-
if err != nil || ident.Handle.IsInvalidHandle() {
58
-
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
59
-
return
60
-
}
61
-
62
-
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
63
-
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
64
-
if err != nil {
65
-
fail(xrpcerr.GenericError(err))
66
-
return
67
-
}
68
-
69
-
repo := resp.Value.Val.(*tangled.Repo)
70
-
didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name)
71
-
if err != nil {
72
-
fail(xrpcerr.GenericError(err))
73
-
return
74
-
}
75
-
76
-
// TODO: fine-grained role based control
77
-
isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid.String(), rbac.ThisServer, didSlashRepo)
52
+
isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid, repoAt)
78
53
if err != nil || !isRepoOwner {
79
54
fail(xrpcerr.AccessControlError(actorDid.String()))
80
55
return
+1
-2
spindle/xrpc/remove_secret.go
+1
-2
spindle/xrpc/remove_secret.go
···
10
10
"github.com/bluesky-social/indigo/xrpc"
11
11
securejoin "github.com/cyphar/filepath-securejoin"
12
12
"tangled.org/core/api/tangled"
13
-
"tangled.org/core/rbac"
14
13
"tangled.org/core/spindle/secrets"
15
14
xrpcerr "tangled.org/core/xrpc/errors"
16
15
)
···
62
61
return
63
62
}
64
63
65
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
64
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
66
65
l.Error("insufficent permissions", "did", actorDid.String())
67
66
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
67
return
+2
-2
spindle/xrpc/xrpc.go
+2
-2
spindle/xrpc/xrpc.go
···
11
11
"tangled.org/core/api/tangled"
12
12
"tangled.org/core/idresolver"
13
13
"tangled.org/core/notifier"
14
-
"tangled.org/core/rbac"
14
+
"tangled.org/core/rbac2"
15
15
"tangled.org/core/spindle/config"
16
16
"tangled.org/core/spindle/db"
17
17
"tangled.org/core/spindle/models"
···
25
25
type Xrpc struct {
26
26
Logger *slog.Logger
27
27
Db *db.DB
28
-
Enforcer *rbac.Enforcer
28
+
Enforcer *rbac2.Enforcer
29
29
Engines map[string]models.Engine
30
30
Config *config.Config
31
31
Resolver *idresolver.Resolver