coverage~bigbes/sr-ht-doltede3b0bbgraph/resolver.go

Coverage
95.0% 19/20 statements
Δ
Blob
05e30ba
Uncovered L75
1 package graph
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "log/slog"
8
9 "go.bigb.es/auxilia/scribe"
10
11 "sourcecraft.dev/bigbes/sr-ht-dolt/authn"
12 "sourcecraft.dev/bigbes/sr-ht-dolt/core"
13 "sourcecraft.dev/bigbes/sr-ht-dolt/db"
14 )
15
16 // Resolver holds what every field of the schema resolves through. It is built
17 // once by New and is safe for concurrent use: it owns no per-request state, and
18 // the caller is read from the context the transport installed.
19 type Resolver struct {
20 repos Repos
21 opener BrowseOpener
22 }
23
24 // defaultLogLimit is how many commits Database.log answers when the query names
25 // no limit, and maxLogLimit is the most it will answer for any limit. The cap is
26 // the surface's and not the client's: a log page is a walk of the on-disk store,
27 // and an unbounded limit is a way to ask this service to walk all of history in
28 // one request.
29 const (
30 defaultLogLimit = 20
31 maxLogLimit = 200
32 )
33
34 // errUnavailable is what a resolver answers when it could not read, as opposed
35 // to when there was nothing to read. The two are never merged: "I could not
36 // check" reported as "there is none" is how a client learns a false fact about
37 // the instance and acts on it.
38 var errUnavailable = errors.New("the database could not be read, try again")
39
40 // internalError logs the cause against the field that produced it and returns
41 // the sentence a client sees. The cause never travels: the store layer's errors
42 // carry on-disk paths and the metadata layer's carry connection strings.
43 3 func internalError(field string, err error) error {
44 3 slog.Error("a GraphQL field could not be resolved",
45 3 "component", "graph", "field", field, scribe.Err(err))
46 3 return errUnavailable
47 3 }
48
49 // callerOf is the request's principal as the access matrix wants it: nil for an
50 // anonymous caller, which is a normal caller on this schema.
51 30 func callerOf(ctx context.Context) *core.Caller {
52 30 return authn.AsCoreCaller(authn.CallerFromContext(ctx))
53 30 }
54
55 // resolveDatabase turns an owner and a name into a row the caller may read, or
56 // into (nil, nil) — "no such database" — which is the schema's single refusal.
57 //
58 // It is the browse handlers' dance call for call (web's loadRepoForBrowse,
59 // mcpsrv's resolveDatabase): the row, the caller's ACL grant, core.Allowed for
60 // OpBrowse. Like the MCP surface and unlike the web, there is no forbidden arm:
61 // a caller who may not read a database is told it does not exist, so its
62 // existence cannot be read out of the shape of the refusal.
63 //
64 // An ACL lookup that fails is an error rather than a fall-through to
65 // visibility. On a page that degradation costs a signed-in user a rendering;
66 // here it would tell a client that a database it holds a grant on is not there.
67 12 func (r *Resolver) resolveDatabase(ctx context.Context, owner, name string) (*core.Repo, error) {
68 12 caller := callerOf(ctx)
69 12
70 12 repo, err := r.repos.GetRepoByOwnerAndName(ctx, owner, name)
71 12 if err != nil {
72 1 if errors.Is(err, db.ErrNotFound) {
73 1 return nil, nil
74 1 }
75 0 return nil, internalError("database", err)
76 }
77
78 // An anonymous caller holds no ACL entry and there is no user id to look one
79 // up by; visibility decides alone, which is what core.Allowed does with a
80 // nil grant.
81 11 var mode *core.AccessMode
82 11 if caller != nil {
83 6 if mode, err = r.repos.EffectiveAccess(ctx, caller.UserID, repo.ID); err != nil {
84 1 return nil, internalError("database", err)
85 1 }
86 }
87 10 if !core.Allowed(caller, repo, mode, core.OpBrowse) {
88 2 return nil, nil
89 2 }
90 8 return repo, nil
91 }
92
93 // openStore opens the bare store of a database whose row the caller has already
94 // been allowed to read. The caller closes the session.
95 9 func (r *Resolver) openStore(ctx context.Context, field string, repo *core.Repo) (BrowseSession, error) {
96 9 sess, err := r.opener.Open(ctx, repo.Path)
97 9 if err != nil {
98 1 return nil, internalError(fmt.Sprintf("Database.%s", field), err)
99 1 }
100 8 return sess, nil
101 }