+11
-7
knotserver/handler.go
+11
-7
knotserver/handler.go
···
16
16
tlog "tangled.sh/tangled.sh/core/log"
17
17
"tangled.sh/tangled.sh/core/notifier"
18
18
"tangled.sh/tangled.sh/core/rbac"
19
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
19
20
)
20
21
21
22
type Handle struct {
···
170
171
func (h *Handle) XrpcRouter() http.Handler {
171
172
logger := tlog.New("knots")
172
173
174
+
serviceAuth := serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String())
175
+
173
176
xrpc := &xrpc.Xrpc{
174
-
Config: h.c,
175
-
Db: h.db,
176
-
Ingester: h.jc,
177
-
Enforcer: h.e,
178
-
Logger: logger,
179
-
Notifier: h.n,
180
-
Resolver: h.resolver,
177
+
Config: h.c,
178
+
Db: h.db,
179
+
Ingester: h.jc,
180
+
Enforcer: h.e,
181
+
Logger: logger,
182
+
Notifier: h.n,
183
+
Resolver: h.resolver,
184
+
ServiceAuth: serviceAuth,
181
185
}
182
186
return xrpc.Router()
183
187
}
+12
-114
knotserver/xrpc/router.go
+12
-114
knotserver/xrpc/router.go
···
1
1
package xrpc
2
2
3
3
import (
4
-
"context"
5
4
"encoding/json"
6
-
"fmt"
7
5
"log/slog"
8
6
"net/http"
9
-
"strings"
10
7
11
8
"tangled.sh/tangled.sh/core/api/tangled"
12
9
"tangled.sh/tangled.sh/core/idresolver"
···
15
12
"tangled.sh/tangled.sh/core/knotserver/db"
16
13
"tangled.sh/tangled.sh/core/notifier"
17
14
"tangled.sh/tangled.sh/core/rbac"
15
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
16
+
"tangled.sh/tangled.sh/core/xrpc/serviceauth"
18
17
19
-
"github.com/bluesky-social/indigo/atproto/auth"
20
18
"github.com/go-chi/chi/v5"
21
19
)
22
20
23
21
type Xrpc struct {
24
-
Config *config.Config
25
-
Db *db.DB
26
-
Ingester *jetstream.JetstreamClient
27
-
Enforcer *rbac.Enforcer
28
-
Logger *slog.Logger
29
-
Notifier *notifier.Notifier
30
-
Resolver *idresolver.Resolver
22
+
Config *config.Config
23
+
Db *db.DB
24
+
Ingester *jetstream.JetstreamClient
25
+
Enforcer *rbac.Enforcer
26
+
Logger *slog.Logger
27
+
Notifier *notifier.Notifier
28
+
Resolver *idresolver.Resolver
29
+
ServiceAuth *serviceauth.ServiceAuth
31
30
}
32
31
33
32
func (x *Xrpc) Router() http.Handler {
34
33
r := chi.NewRouter()
35
34
36
-
r.With(x.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
35
+
r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
37
36
38
37
return r
39
38
}
40
39
41
-
func (x *Xrpc) VerifyServiceAuth(next http.Handler) http.Handler {
42
-
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43
-
l := x.Logger.With("url", r.URL)
44
-
45
-
token := r.Header.Get("Authorization")
46
-
token = strings.TrimPrefix(token, "Bearer ")
47
-
48
-
s := auth.ServiceAuthValidator{
49
-
Audience: x.Config.Server.Did().String(),
50
-
Dir: x.Resolver.Directory(),
51
-
}
52
-
53
-
did, err := s.Validate(r.Context(), token, nil)
54
-
if err != nil {
55
-
l.Error("signature verification failed", "err", err)
56
-
writeError(w, AuthError(err), http.StatusForbidden)
57
-
return
58
-
}
59
-
60
-
r = r.WithContext(
61
-
context.WithValue(r.Context(), ActorDid, did),
62
-
)
63
-
64
-
next.ServeHTTP(w, r)
65
-
})
66
-
}
67
-
68
-
type XrpcError struct {
69
-
Tag string `json:"error"`
70
-
Message string `json:"message"`
71
-
}
72
-
73
-
func NewXrpcError(opts ...ErrOpt) XrpcError {
74
-
x := XrpcError{}
75
-
for _, o := range opts {
76
-
o(&x)
77
-
}
78
-
79
-
return x
80
-
}
81
-
82
-
type ErrOpt = func(xerr *XrpcError)
83
-
84
-
func WithTag(tag string) ErrOpt {
85
-
return func(xerr *XrpcError) {
86
-
xerr.Tag = tag
87
-
}
88
-
}
89
-
90
-
func WithMessage[S ~string](s S) ErrOpt {
91
-
return func(xerr *XrpcError) {
92
-
xerr.Message = string(s)
93
-
}
94
-
}
95
-
96
-
func WithError(e error) ErrOpt {
97
-
return func(xerr *XrpcError) {
98
-
xerr.Message = e.Error()
99
-
}
100
-
}
101
-
102
-
var MissingActorDidError = NewXrpcError(
103
-
WithTag("MissingActorDid"),
104
-
WithMessage("actor DID not supplied"),
105
-
)
106
-
107
-
var AuthError = func(err error) XrpcError {
108
-
return NewXrpcError(
109
-
WithTag("Auth"),
110
-
WithError(fmt.Errorf("signature verification failed: %w", err)),
111
-
)
112
-
}
113
-
114
-
var InvalidRepoError = func(r string) XrpcError {
115
-
return NewXrpcError(
116
-
WithTag("InvalidRepo"),
117
-
WithError(fmt.Errorf("supplied at-uri is not a repo: %s", r)),
118
-
)
119
-
}
120
-
121
-
var AccessControlError = func(d string) XrpcError {
122
-
return NewXrpcError(
123
-
WithTag("AccessControl"),
124
-
WithError(fmt.Errorf("DID does not have sufficent access permissions for this operation: %s", d)),
125
-
)
126
-
}
127
-
128
-
var GitError = func(e error) XrpcError {
129
-
return NewXrpcError(
130
-
WithTag("Git"),
131
-
WithError(fmt.Errorf("git error: %w", e)),
132
-
)
133
-
}
134
-
135
-
func GenericError(err error) XrpcError {
136
-
return NewXrpcError(
137
-
WithTag("Generic"),
138
-
WithError(err),
139
-
)
140
-
}
141
-
142
40
// this is slightly different from http_util::write_error to follow the spec:
143
41
//
144
42
// the json object returned must include an "error" and a "message"
145
-
func writeError(w http.ResponseWriter, e XrpcError, status int) {
43
+
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
146
44
w.Header().Set("Content-Type", "application/json")
147
45
w.WriteHeader(status)
148
46
json.NewEncoder(w).Encode(e)
+12
-10
knotserver/xrpc/set_default_branch.go
+12
-10
knotserver/xrpc/set_default_branch.go
···
12
12
"tangled.sh/tangled.sh/core/api/tangled"
13
13
"tangled.sh/tangled.sh/core/knotserver/git"
14
14
"tangled.sh/tangled.sh/core/rbac"
15
+
16
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
15
17
)
16
18
17
19
const ActorDid string = "ActorDid"
18
20
19
21
func (x *Xrpc) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
20
22
l := x.Logger
21
-
fail := func(e XrpcError) {
23
+
fail := func(e xrpcerr.XrpcError) {
22
24
l.Error("failed", "kind", e.Tag, "error", e.Message)
23
25
writeError(w, e, http.StatusBadRequest)
24
26
}
25
27
26
28
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
27
29
if !ok {
28
-
fail(MissingActorDidError)
30
+
fail(xrpcerr.MissingActorDidError)
29
31
return
30
32
}
31
33
32
34
var data tangled.RepoSetDefaultBranch_Input
33
35
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
34
-
fail(GenericError(err))
36
+
fail(xrpcerr.GenericError(err))
35
37
return
36
38
}
37
39
38
40
// unfortunately we have to resolve repo-at here
39
41
repoAt, err := syntax.ParseATURI(data.Repo)
40
42
if err != nil {
41
-
fail(InvalidRepoError(data.Repo))
43
+
fail(xrpcerr.InvalidRepoError(data.Repo))
42
44
return
43
45
}
44
46
45
47
// resolve this aturi to extract the repo record
46
48
ident, err := x.Resolver.ResolveIdent(r.Context(), repoAt.Authority().String())
47
49
if err != nil || ident.Handle.IsInvalidHandle() {
48
-
fail(GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
50
+
fail(xrpcerr.GenericError(fmt.Errorf("failed to resolve handle: %w", err)))
49
51
return
50
52
}
51
53
52
54
xrpcc := xrpc.Client{Host: ident.PDSEndpoint()}
53
55
resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
54
56
if err != nil {
55
-
fail(GenericError(err))
57
+
fail(xrpcerr.GenericError(err))
56
58
return
57
59
}
58
60
59
61
repo := resp.Value.Val.(*tangled.Repo)
60
62
didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name)
61
63
if err != nil {
62
-
fail(GenericError(err))
64
+
fail(xrpcerr.GenericError(err))
63
65
return
64
66
}
65
67
66
68
if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
67
69
l.Error("insufficent permissions", "did", actorDid.String())
68
-
writeError(w, AccessControlError(actorDid.String()), http.StatusUnauthorized)
70
+
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
69
71
return
70
72
}
71
73
72
74
path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath)
73
75
gr, err := git.PlainOpen(path)
74
76
if err != nil {
75
-
fail(InvalidRepoError(data.Repo))
77
+
fail(xrpcerr.GenericError(err))
76
78
return
77
79
}
78
80
79
81
err = gr.SetDefaultBranch(data.DefaultBranch)
80
82
if err != nil {
81
83
l.Error("setting default branch", "error", err.Error())
82
-
writeError(w, GitError(err), http.StatusInternalServerError)
84
+
writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError)
83
85
return
84
86
}
85
87