+11
-7
spindle/server.go
+11
-7
spindle/server.go
···
25
25
"tangled.sh/tangled.sh/core/spindle/queue"
26
26
"tangled.sh/tangled.sh/core/spindle/secrets"
27
27
"tangled.sh/tangled.sh/core/spindle/xrpc"
28
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
28
29
)
29
30
30
31
//go:embed motd
···
213
214
func (s *Spindle) XrpcRouter() http.Handler {
214
215
logger := s.l.With("route", "xrpc")
215
216
217
+
serviceAuth := serviceauth.NewServiceAuth(s.l, s.res, s.cfg.Server.Did().String())
218
+
216
219
x := xrpc.Xrpc{
217
-
Logger: logger,
218
-
Db: s.db,
219
-
Enforcer: s.e,
220
-
Engines: s.engs,
221
-
Config: s.cfg,
222
-
Resolver: s.res,
223
-
Vault: s.vault,
220
+
Logger: logger,
221
+
Db: s.db,
222
+
Enforcer: s.e,
223
+
Engines: s.engs,
224
+
Config: s.cfg,
225
+
Resolver: s.res,
226
+
Vault: s.vault,
227
+
ServiceAuth: serviceAuth,
224
228
}
225
229
226
230
return x.Router()
+11
-10
spindle/xrpc/add_secret.go
+11
-10
spindle/xrpc/add_secret.go
···
13
13
"tangled.sh/tangled.sh/core/api/tangled"
14
14
"tangled.sh/tangled.sh/core/rbac"
15
15
"tangled.sh/tangled.sh/core/spindle/secrets"
16
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
16
17
)
17
18
18
19
func (x *Xrpc) AddSecret(w http.ResponseWriter, r *http.Request) {
19
20
l := x.Logger
20
-
fail := func(e XrpcError) {
21
+
fail := func(e xrpcerr.XrpcError) {
21
22
l.Error("failed", "kind", e.Tag, "error", e.Message)
22
23
writeError(w, e, http.StatusBadRequest)
23
24
}
24
25
25
26
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26
27
if !ok {
27
-
fail(MissingActorDidError)
28
+
fail(xrpcerr.MissingActorDidError)
28
29
return
29
30
}
30
31
31
32
var data tangled.RepoAddSecret_Input
32
33
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
33
-
fail(GenericError(err))
34
+
fail(xrpcerr.GenericError(err))
34
35
return
35
36
}
36
37
37
38
if err := secrets.ValidateKey(data.Key); err != nil {
38
-
fail(GenericError(err))
39
+
fail(xrpcerr.GenericError(err))
39
40
return
40
41
}
41
42
42
43
// unfortunately we have to resolve repo-at here
43
44
repoAt, err := syntax.ParseATURI(data.Repo)
44
45
if err != nil {
45
-
fail(InvalidRepoError(data.Repo))
46
+
fail(xrpcerr.InvalidRepoError(data.Repo))
46
47
return
47
48
}
48
49
49
50
// resolve this aturi to extract the repo record
50
51
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
51
52
if err != nil || ident.Handle.IsInvalidHandle() {
52
-
fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
53
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
53
54
return
54
55
}
55
56
56
57
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
57
58
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
58
59
if err != nil {
59
-
fail(GenericError(err))
60
+
fail(xrpcerr.GenericError(err))
60
61
return
61
62
}
62
63
63
64
repo := resp.Value.Val.(*tangled.Repo)
64
65
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
65
66
if err != nil {
66
-
fail(GenericError(err))
67
+
fail(xrpcerr.GenericError(err))
67
68
return
68
69
}
69
70
70
71
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
71
72
l.Error("insufficent permissions", "did", actorDid.String())
72
-
writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)
73
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
73
74
return
74
75
}
75
76
···
83
84
err = x.Vault.AddSecret(r.Context(), secret)
84
85
if err != nil {
85
86
l.Error("failed to add secret to vault", "did", actorDid.String(), "err", err)
86
-
writeError(w, GenericError(err), http.StatusInternalServerError)
87
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
87
88
return
88
89
}
89
90
+10
-9
spindle/xrpc/list_secrets.go
+10
-9
spindle/xrpc/list_secrets.go
···
13
13
"tangled.sh/tangled.sh/core/api/tangled"
14
14
"tangled.sh/tangled.sh/core/rbac"
15
15
"tangled.sh/tangled.sh/core/spindle/secrets"
16
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
16
17
)
17
18
18
19
func (x *Xrpc) ListSecrets(w http.ResponseWriter, r *http.Request) {
19
20
l := x.Logger
20
-
fail := func(e XrpcError) {
21
+
fail := func(e xrpcerr.XrpcError) {
21
22
l.Error("failed", "kind", e.Tag, "error", e.Message)
22
23
writeError(w, e, http.StatusBadRequest)
23
24
}
24
25
25
26
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
26
27
if !ok {
27
-
fail(MissingActorDidError)
28
+
fail(xrpcerr.MissingActorDidError)
28
29
return
29
30
}
30
31
31
32
repoParam := r.URL.Query().Get("repo")
32
33
if repoParam == "" {
33
-
fail(GenericError(fmt.Errorf("empty params")))
34
+
fail(xrpcerr.GenericError(fmt.Errorf("empty params")))
34
35
return
35
36
}
36
37
37
38
// unfortunately we have to resolve repo-at here
38
39
repoAt, err := syntax.ParseATURI(repoParam)
39
40
if err != nil {
40
-
fail(InvalidRepoError(repoParam))
41
+
fail(xrpcerr.InvalidRepoError(repoParam))
41
42
return
42
43
}
43
44
44
45
// resolve this aturi to extract the repo record
45
46
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
46
47
if err != nil || ident.Handle.IsInvalidHandle() {
47
-
fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
48
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
48
49
return
49
50
}
50
51
51
52
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
52
53
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
53
54
if err != nil {
54
-
fail(GenericError(err))
55
+
fail(xrpcerr.GenericError(err))
55
56
return
56
57
}
57
58
58
59
repo := resp.Value.Val.(*tangled.Repo)
59
60
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
60
61
if err != nil {
61
-
fail(GenericError(err))
62
+
fail(xrpcerr.GenericError(err))
62
63
return
63
64
}
64
65
65
66
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
66
67
l.Error("insufficent permissions", "did", actorDid.String())
67
-
writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
69
return
69
70
}
70
71
71
72
ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.DidSlashRepo(didPath))
72
73
if err != nil {
73
74
l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err)
74
-
writeError(w, GenericError(err), http.StatusInternalServerError)
75
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
75
76
return
76
77
}
77
78
+10
-9
spindle/xrpc/remove_secret.go
+10
-9
spindle/xrpc/remove_secret.go
···
12
12
"tangled.sh/tangled.sh/core/api/tangled"
13
13
"tangled.sh/tangled.sh/core/rbac"
14
14
"tangled.sh/tangled.sh/core/spindle/secrets"
15
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
15
16
)
16
17
17
18
func (x *Xrpc) RemoveSecret(w http.ResponseWriter, r *http.Request) {
18
19
l := x.Logger
19
-
fail := func(e XrpcError) {
20
+
fail := func(e xrpcerr.XrpcError) {
20
21
l.Error("failed", "kind", e.Tag, "error", e.Message)
21
22
writeError(w, e, http.StatusBadRequest)
22
23
}
23
24
24
25
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
25
26
if !ok {
26
-
fail(MissingActorDidError)
27
+
fail(xrpcerr.MissingActorDidError)
27
28
return
28
29
}
29
30
30
31
var data tangled.RepoRemoveSecret_Input
31
32
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
32
-
fail(GenericError(err))
33
+
fail(xrpcerr.GenericError(err))
33
34
return
34
35
}
35
36
36
37
// unfortunately we have to resolve repo-at here
37
38
repoAt, err := syntax.ParseATURI(data.Repo)
38
39
if err != nil {
39
-
fail(InvalidRepoError(data.Repo))
40
+
fail(xrpcerr.InvalidRepoError(data.Repo))
40
41
return
41
42
}
42
43
43
44
// resolve this aturi to extract the repo record
44
45
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
45
46
if err != nil || ident.Handle.IsInvalidHandle() {
46
-
fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
47
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
47
48
return
48
49
}
49
50
50
51
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
51
52
resp, err := atproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
52
53
if err != nil {
53
-
fail(GenericError(err))
54
+
fail(xrpcerr.GenericError(err))
54
55
return
55
56
}
56
57
57
58
repo := resp.Value.Val.(*tangled.Repo)
58
59
didPath, err := securejoin.SecureJoin(repo.Owner, repo.Name)
59
60
if err != nil {
60
-
fail(GenericError(err))
61
+
fail(xrpcerr.GenericError(err))
61
62
return
62
63
}
63
64
64
65
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
65
66
l.Error("insufficent permissions", "did", actorDid.String())
66
-
writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)
67
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
67
68
return
68
69
}
69
70
···
74
75
err = x.Vault.RemoveSecret(r.Context(), secret)
75
76
if err != nil {
76
77
l.Error("failed to remove secret from vault", "did", actorDid.String(), "err", err)
77
-
writeError(w, GenericError(err), http.StatusInternalServerError)
78
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
78
79
return
79
80
}
80
81
+14
-109
spindle/xrpc/xrpc.go
+14
-109
spindle/xrpc/xrpc.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"context"
5
4
_ "embed"
6
5
"encoding/json"
7
-
"fmt"
8
6
"log/slog"
9
7
"net/http"
10
-
"strings"
11
8
12
-
"github.com/bluesky-social/indigo/atproto/auth"
13
9
"github.com/go-chi/chi/v5"
14
10
15
11
"tangled.sh/tangled.sh/core/api/tangled"
···
19
15
"tangled.sh/tangled.sh/core/spindle/db"
20
16
"tangled.sh/tangled.sh/core/spindle/models"
21
17
"tangled.sh/tangled.sh/core/spindle/secrets"
18
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
19
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
22
20
)
23
21
24
22
const ActorDid string = "ActorDid"
25
23
26
24
type Xrpc struct {
27
-
Logger *slog.Logger
28
-
Db *db.DB
29
-
Enforcer *rbac.Enforcer
30
-
Engines map[string]models.Engine
31
-
Config *config.Config
32
-
Resolver *idresolver.Resolver
33
-
Vault secrets.Manager
25
+
Logger *slog.Logger
26
+
Db *db.DB
27
+
Enforcer *rbac.Enforcer
28
+
Engines map[string]models.Engine
29
+
Config *config.Config
30
+
Resolver *idresolver.Resolver
31
+
Vault secrets.Manager
32
+
ServiceAuth *serviceauth.ServiceAuth
34
33
}
35
34
36
35
func (x *Xrpc) Router() http.Handler {
37
36
r := chi.NewRouter()
38
37
39
-
r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
40
-
r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
41
-
r.With(x.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
38
+
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
39
+
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
40
+
r.With(x.ServiceAuth.VerifyServiceAuth).Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
42
41
43
42
return r
44
43
}
45
44
46
-
func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler {
47
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
48
-
l := x.Logger.With("url", r.URL)
49
-
50
-
token := r.Header.Get("Authorization")
51
-
token = strings.TrimPrefix(token, "Bearer ")
52
-
53
-
s := auth.ServiceAuthValidator{
54
-
Audience: x.Config.Server.Did().String(),
55
-
Dir: x.Resolver.Directory(),
56
-
}
57
-
58
-
did, err := s.Validate(r.Context(), token, nil)
59
-
if err != nil {
60
-
l.Error("signature verification failed", "err", err)
61
-
writeError(w, AuthError(err), http.StatusForbidden)
62
-
return
63
-
}
64
-
65
-
r = r.WithContext(
66
-
context.WithValue(r.Context(), ActorDid, did),
67
-
)
68
-
69
-
next.ServeHTTP(w, r)
70
-
})
71
-
}
72
-
73
-
type XrpcError struct {
74
-
Tag string `json:"error"`
75
-
Message string `json:"message"`
76
-
}
77
-
78
-
func NewXrpcError(opts ...ErrOpt) XrpcError {
79
-
x := XrpcError{}
80
-
for _, o := range opts {
81
-
o(&x)
82
-
}
83
-
84
-
return x
85
-
}
86
-
87
-
type ErrOpt = func(xerr *XrpcError)
88
-
89
-
func WithTag(tag string) ErrOpt {
90
-
return func(xerr *XrpcError) {
91
-
xerr.Tag = tag
92
-
}
93
-
}
94
-
95
-
func WithMessage[S ~string](s S) ErrOpt {
96
-
return func(xerr *XrpcError) {
97
-
xerr.Message = string(s)
98
-
}
99
-
}
100
-
101
-
func WithError(e error) ErrOpt {
102
-
return func(xerr *XrpcError) {
103
-
xerr.Message = e.Error()
104
-
}
105
-
}
106
-
107
-
var MissingActorDidError = NewXrpcError(
108
-
WithTag("MissingActorDid"),
109
-
WithMessage("actor DID not supplied"),
110
-
)
111
-
112
-
var AuthError = func(err error) XrpcError {
113
-
return NewXrpcError(
114
-
WithTag("Auth"),
115
-
WithError(fmt.Errorf("signature verification failed: %w", err)),
116
-
)
117
-
}
118
-
119
-
var InvalidRepoError = func(r string) XrpcError {
120
-
return NewXrpcError(
121
-
WithTag("InvalidRepo"),
122
-
WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)),
123
-
)
124
-
}
125
-
126
-
func GenericError(err error) XrpcError {
127
-
return NewXrpcError(
128
-
WithTag("Generic"),
129
-
WithError(err),
130
-
)
131
-
}
132
-
133
-
var AccessControlError = func(d string) XrpcError {
134
-
return NewXrpcError(
135
-
WithTag("AccessControl"),
136
-
WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)),
137
-
)
138
-
}
139
-
140
45
// this is slightly different from http_util::write_error to follow the spec:
141
46
//
142
47
// the json object returned must include an "error" and a "message"
143
-
func writeError(w http.ResponseWriter, e XrpcError, status int) {
48
+
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
144
49
w.Header().Set("Content-Type", "application/json")
145
50
w.WriteHeader(status)
146
51
json.NewEncoder(w).Encode(e)