+11
-11
spindle/config/config.go
+11
-11
spindle/config/config.go
···
9
)
10
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
23
}
24
25
func (s Server) Did() syntax.DID {
···
9
)
10
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 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
}
24
25
func (s Server) Did() syntax.DID {
+17
-41
spindle/ingester.go
+17
-41
spindle/ingester.go
···
9
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/eventconsumer"
12
-
"tangled.org/core/rbac"
13
"tangled.org/core/spindle/db"
14
15
comatproto "github.com/bluesky-social/indigo/api/atproto"
16
-
"github.com/bluesky-social/indigo/atproto/identity"
17
"github.com/bluesky-social/indigo/atproto/syntax"
18
"github.com/bluesky-social/indigo/xrpc"
19
"github.com/bluesky-social/jetstream/pkg/models"
20
-
securejoin "github.com/cyphar/filepath-securejoin"
21
)
22
23
type Ingester func(ctx context.Context, e *models.Event) error
···
79
return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain)
80
}
81
82
-
ok, err := s.e.IsSpindleInviteAllowed(did, rbacDomain)
83
if err != nil || !ok {
84
l.Error("failed to add member", "did", did, "error", err)
85
return fmt.Errorf("failed to enforce permissions: %w", err)
···
96
return fmt.Errorf("failed to add member: %w", err)
97
}
98
99
-
if err := s.e.AddSpindleMember(rbacDomain, record.Subject); err != nil {
100
l.Error("failed to add member", "error", err)
101
return fmt.Errorf("failed to add member: %w", err)
102
}
···
122
return fmt.Errorf("failed to remove member: %w", err)
123
}
124
125
-
if err := s.e.RemoveSpindleMember(rbacDomain, record.Subject.String()); err != nil {
126
l.Error("failed to add member", "error", err)
127
return fmt.Errorf("failed to add member: %w", err)
128
}
···
176
return fmt.Errorf("failed to add repo: %w", err)
177
}
178
179
-
didSlashRepo, err := securejoin.SecureJoin(did, record.Name)
180
-
if err != nil {
181
-
return err
182
-
}
183
184
// add repo to rbac
185
-
if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil {
186
l.Error("failed to add repo to enforcer", "error", err)
187
return fmt.Errorf("failed to add repo: %w", err)
188
}
189
190
// 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 {
196
return err
197
}
198
···
234
return nil
235
}
236
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
// check perms for this user
257
-
if ok, err := s.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil {
258
return fmt.Errorf("insufficient permissions: %w", err)
259
}
260
261
// add collaborator to rbac
262
-
if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil {
263
l.Error("failed to add repo to enforcer", "error", err)
264
return fmt.Errorf("failed to add repo: %w", err)
265
}
···
269
return nil
270
}
271
272
-
func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error {
273
l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators")
274
275
l.Info("fetching and adding existing collaborators")
276
277
xrpcc := xrpc.Client{
278
-
Host: owner.PDSEndpoint(),
279
}
280
281
-
resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, owner.DID.String(), false)
282
if err != nil {
283
return err
284
}
···
290
}
291
record := r.Value.Val.(*tangled.RepoCollaborator)
292
293
-
if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil {
294
l.Error("failed to add repo to enforcer", "error", err)
295
errors.Join(errs, fmt.Errorf("failed to add repo: %w", err))
296
}
···
9
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/eventconsumer"
12
"tangled.org/core/spindle/db"
13
14
comatproto "github.com/bluesky-social/indigo/api/atproto"
15
"github.com/bluesky-social/indigo/atproto/syntax"
16
"github.com/bluesky-social/indigo/xrpc"
17
"github.com/bluesky-social/jetstream/pkg/models"
18
)
19
20
type Ingester func(ctx context.Context, e *models.Event) error
···
76
return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain)
77
}
78
79
+
ok, err := s.e.IsSpindleMemberInviteAllowed(syntax.DID(did), s.cfg.Server.Did())
80
if err != nil || !ok {
81
l.Error("failed to add member", "did", did, "error", err)
82
return fmt.Errorf("failed to enforce permissions: %w", err)
···
93
return fmt.Errorf("failed to add member: %w", err)
94
}
95
96
+
if err := s.e.AddSpindleMember(syntax.DID(record.Subject), s.cfg.Server.Did()); err != nil {
97
l.Error("failed to add member", "error", err)
98
return fmt.Errorf("failed to add member: %w", err)
99
}
···
119
return fmt.Errorf("failed to remove member: %w", err)
120
}
121
122
+
if err := s.e.RemoveSpindleMember(record.Subject, s.cfg.Server.Did()); err != nil {
123
l.Error("failed to add member", "error", err)
124
return fmt.Errorf("failed to add member: %w", err)
125
}
···
173
return fmt.Errorf("failed to add repo: %w", err)
174
}
175
176
+
repoAt := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, e.Commit.Collection, e.Commit.RKey))
177
178
// add repo to rbac
179
+
if err := s.e.AddRepo(repoAt); err != nil {
180
l.Error("failed to add repo to enforcer", "error", err)
181
return fmt.Errorf("failed to add repo: %w", err)
182
}
183
184
// add collaborators to rbac
185
+
if err := s.fetchAndAddCollaborators(ctx, repoAt); err != nil {
186
return err
187
}
188
···
224
return nil
225
}
226
227
// check perms for this user
228
+
if ok, err := s.e.IsRepoCollaboratorInviteAllowed(syntax.DID(e.Did), repoAt); !ok || err != nil {
229
return fmt.Errorf("insufficient permissions: %w", err)
230
}
231
232
// add collaborator to rbac
233
+
if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), repoAt); err != nil {
234
l.Error("failed to add repo to enforcer", "error", err)
235
return fmt.Errorf("failed to add repo: %w", err)
236
}
···
240
return nil
241
}
242
243
+
func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, repo syntax.ATURI) error {
244
l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators")
245
246
l.Info("fetching and adding existing collaborators")
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
+
253
xrpcc := xrpc.Client{
254
+
Host: ident.PDSEndpoint(),
255
}
256
257
+
resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, ident.DID.String(), false)
258
if err != nil {
259
return err
260
}
···
266
}
267
record := r.Value.Val.(*tangled.RepoCollaborator)
268
269
+
if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)); err != nil {
270
l.Error("failed to add repo to enforcer", "error", err)
271
errors.Join(errs, fmt.Errorf("failed to add repo: %w", err))
272
}
+6
-47
spindle/server.go
+6
-47
spindle/server.go
···
17
"tangled.org/core/jetstream"
18
"tangled.org/core/log"
19
"tangled.org/core/notifier"
20
-
"tangled.org/core/rbac"
21
"tangled.org/core/spindle/config"
22
"tangled.org/core/spindle/db"
23
"tangled.org/core/spindle/engine"
···
32
//go:embed motd
33
var motd []byte
34
35
-
const (
36
-
rbacDomain = "thisserver"
37
-
)
38
-
39
type Spindle struct {
40
jc *jetstream.JetstreamClient
41
db *db.DB
42
-
e *rbac.Enforcer
43
l *slog.Logger
44
n *notifier.Notifier
45
engs map[string]models.Engine
···
59
return nil, fmt.Errorf("failed to setup db: %w", err)
60
}
61
62
-
e, err := rbac.NewEnforcer(cfg.Server.DBPath)
63
if err != nil {
64
return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err)
65
}
66
-
e.E.EnableAutoSave(true)
67
68
n := notifier.New()
69
···
104
if err != nil {
105
return nil, fmt.Errorf("failed to setup jetstream client: %w", err)
106
}
107
-
jc.AddDid(cfg.Server.Owner)
108
109
// Check if the spindle knows about any Dids;
110
dids, err := d.GetAllDids()
···
130
vault: vault,
131
}
132
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()
138
if err != nil {
139
return nil, err
140
}
···
197
}
198
199
// Enforcer returns the RBAC enforcer instance.
200
-
func (s *Spindle) Enforcer() *rbac.Enforcer {
201
return s.e
202
}
203
···
382
383
return nil
384
}
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
-
}
···
17
"tangled.org/core/jetstream"
18
"tangled.org/core/log"
19
"tangled.org/core/notifier"
20
+
"tangled.org/core/rbac2"
21
"tangled.org/core/spindle/config"
22
"tangled.org/core/spindle/db"
23
"tangled.org/core/spindle/engine"
···
32
//go:embed motd
33
var motd []byte
34
35
type Spindle struct {
36
jc *jetstream.JetstreamClient
37
db *db.DB
38
+
e *rbac2.Enforcer
39
l *slog.Logger
40
n *notifier.Notifier
41
engs map[string]models.Engine
···
55
return nil, fmt.Errorf("failed to setup db: %w", err)
56
}
57
58
+
e, err := rbac2.NewEnforcer(cfg.Server.DBPath)
59
if err != nil {
60
return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err)
61
}
62
63
n := notifier.New()
64
···
99
if err != nil {
100
return nil, fmt.Errorf("failed to setup jetstream client: %w", err)
101
}
102
+
jc.AddDid(cfg.Server.Owner.String())
103
104
// Check if the spindle knows about any Dids;
105
dids, err := d.GetAllDids()
···
125
vault: vault,
126
}
127
128
+
err = e.SetSpindleOwner(spindle.cfg.Server.Owner, spindle.cfg.Server.Did())
129
if err != nil {
130
return nil, err
131
}
···
188
}
189
190
// Enforcer returns the RBAC enforcer instance.
191
+
func (s *Spindle) Enforcer() *rbac2.Enforcer {
192
return s.e
193
}
194
···
373
374
return nil
375
}
+1
-2
spindle/xrpc/add_secret.go
+1
-2
spindle/xrpc/add_secret.go
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
"tangled.org/core/spindle/secrets"
16
xrpcerr "tangled.org/core/xrpc/errors"
17
)
···
68
return
69
}
70
71
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
72
l.Error("insufficent permissions", "did", actorDid.String())
73
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
74
return
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/spindle/secrets"
15
xrpcerr "tangled.org/core/xrpc/errors"
16
)
···
67
return
68
}
69
70
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
71
l.Error("insufficent permissions", "did", actorDid.String())
72
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
73
return
+1
-2
spindle/xrpc/list_secrets.go
+1
-2
spindle/xrpc/list_secrets.go
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
"tangled.org/core/spindle/secrets"
16
xrpcerr "tangled.org/core/xrpc/errors"
17
)
···
63
return
64
}
65
66
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
67
l.Error("insufficent permissions", "did", actorDid.String())
68
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
69
return
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/spindle/secrets"
15
xrpcerr "tangled.org/core/xrpc/errors"
16
)
···
62
return
63
}
64
65
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
66
l.Error("insufficent permissions", "did", actorDid.String())
67
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
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
"net/http"
7
"strings"
8
9
-
"github.com/bluesky-social/indigo/api/atproto"
10
"github.com/bluesky-social/indigo/atproto/syntax"
11
-
"github.com/bluesky-social/indigo/xrpc"
12
-
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
"tangled.org/core/spindle/models"
16
xrpcerr "tangled.org/core/xrpc/errors"
17
)
···
53
return
54
}
55
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)
78
if err != nil || !isRepoOwner {
79
fail(xrpcerr.AccessControlError(actorDid.String()))
80
return
···
6
"net/http"
7
"strings"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/spindle/models"
12
xrpcerr "tangled.org/core/xrpc/errors"
13
)
···
49
return
50
}
51
52
+
isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid, repoAt)
53
if err != nil || !isRepoOwner {
54
fail(xrpcerr.AccessControlError(actorDid.String()))
55
return
+1
-2
spindle/xrpc/remove_secret.go
+1
-2
spindle/xrpc/remove_secret.go
···
10
"github.com/bluesky-social/indigo/xrpc"
11
securejoin "github.com/cyphar/filepath-securejoin"
12
"tangled.org/core/api/tangled"
13
-
"tangled.org/core/rbac"
14
"tangled.org/core/spindle/secrets"
15
xrpcerr "tangled.org/core/xrpc/errors"
16
)
···
62
return
63
}
64
65
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
66
l.Error("insufficent permissions", "did", actorDid.String())
67
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
return
···
10
"github.com/bluesky-social/indigo/xrpc"
11
securejoin "github.com/cyphar/filepath-securejoin"
12
"tangled.org/core/api/tangled"
13
"tangled.org/core/spindle/secrets"
14
xrpcerr "tangled.org/core/xrpc/errors"
15
)
···
61
return
62
}
63
64
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
65
l.Error("insufficent permissions", "did", actorDid.String())
66
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
67
return
+2
-2
spindle/xrpc/xrpc.go
+2
-2
spindle/xrpc/xrpc.go
···
11
"tangled.org/core/api/tangled"
12
"tangled.org/core/idresolver"
13
"tangled.org/core/notifier"
14
-
"tangled.org/core/rbac"
15
"tangled.org/core/spindle/config"
16
"tangled.org/core/spindle/db"
17
"tangled.org/core/spindle/models"
···
25
type Xrpc struct {
26
Logger *slog.Logger
27
Db *db.DB
28
-
Enforcer *rbac.Enforcer
29
Engines map[string]models.Engine
30
Config *config.Config
31
Resolver *idresolver.Resolver
···
11
"tangled.org/core/api/tangled"
12
"tangled.org/core/idresolver"
13
"tangled.org/core/notifier"
14
+
"tangled.org/core/rbac2"
15
"tangled.org/core/spindle/config"
16
"tangled.org/core/spindle/db"
17
"tangled.org/core/spindle/models"
···
25
type Xrpc struct {
26
Logger *slog.Logger
27
Db *db.DB
28
+
Enforcer *rbac2.Enforcer
29
Engines map[string]models.Engine
30
Config *config.Config
31
Resolver *idresolver.Resolver