~bigbes/sr-ht-dolt

ref: ede3b0bb2671ffde35a66527b920c40eaccc096f sr-ht-dolt/graph/resolver.go -rw-r--r-- 3.8 KiB
ede3b0bb — Eugene Blikh go.mod: take the shared libraries' current heads 2 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package graph

import (
	"context"
	"errors"
	"fmt"
	"log/slog"

	"go.bigb.es/auxilia/scribe"

	"sourcecraft.dev/bigbes/sr-ht-dolt/authn"
	"sourcecraft.dev/bigbes/sr-ht-dolt/core"
	"sourcecraft.dev/bigbes/sr-ht-dolt/db"
)

// Resolver holds what every field of the schema resolves through. It is built
// once by New and is safe for concurrent use: it owns no per-request state, and
// the caller is read from the context the transport installed.
type Resolver struct {
	repos  Repos
	opener BrowseOpener
}

// defaultLogLimit is how many commits Database.log answers when the query names
// no limit, and maxLogLimit is the most it will answer for any limit. The cap is
// the surface's and not the client's: a log page is a walk of the on-disk store,
// and an unbounded limit is a way to ask this service to walk all of history in
// one request.
const (
	defaultLogLimit = 20
	maxLogLimit     = 200
)

// errUnavailable is what a resolver answers when it could not read, as opposed
// to when there was nothing to read. The two are never merged: "I could not
// check" reported as "there is none" is how a client learns a false fact about
// the instance and acts on it.
var errUnavailable = errors.New("the database could not be read, try again")

// internalError logs the cause against the field that produced it and returns
// the sentence a client sees. The cause never travels: the store layer's errors
// carry on-disk paths and the metadata layer's carry connection strings.
func internalError(field string, err error) error {
	slog.Error("a GraphQL field could not be resolved",
		"component", "graph", "field", field, scribe.Err(err))
	return errUnavailable
}

// callerOf is the request's principal as the access matrix wants it: nil for an
// anonymous caller, which is a normal caller on this schema.
func callerOf(ctx context.Context) *core.Caller {
	return authn.AsCoreCaller(authn.CallerFromContext(ctx))
}

// resolveDatabase turns an owner and a name into a row the caller may read, or
// into (nil, nil) — "no such database" — which is the schema's single refusal.
//
// It is the browse handlers' dance call for call (web's loadRepoForBrowse,
// mcpsrv's resolveDatabase): the row, the caller's ACL grant, core.Allowed for
// OpBrowse. Like the MCP surface and unlike the web, there is no forbidden arm:
// a caller who may not read a database is told it does not exist, so its
// existence cannot be read out of the shape of the refusal.
//
// An ACL lookup that fails is an error rather than a fall-through to
// visibility. On a page that degradation costs a signed-in user a rendering;
// here it would tell a client that a database it holds a grant on is not there.
func (r *Resolver) resolveDatabase(ctx context.Context, owner, name string) (*core.Repo, error) {
	caller := callerOf(ctx)

	repo, err := r.repos.GetRepoByOwnerAndName(ctx, owner, name)
	if err != nil {
		if errors.Is(err, db.ErrNotFound) {
			return nil, nil
		}
		return nil, internalError("database", err)
	}

	// An anonymous caller holds no ACL entry and there is no user id to look one
	// up by; visibility decides alone, which is what core.Allowed does with a
	// nil grant.
	var mode *core.AccessMode
	if caller != nil {
		if mode, err = r.repos.EffectiveAccess(ctx, caller.UserID, repo.ID); err != nil {
			return nil, internalError("database", err)
		}
	}
	if !core.Allowed(caller, repo, mode, core.OpBrowse) {
		return nil, nil
	}
	return repo, nil
}

// openStore opens the bare store of a database whose row the caller has already
// been allowed to read. The caller closes the session.
func (r *Resolver) openStore(ctx context.Context, field string, repo *core.Repo) (BrowseSession, error) {
	sess, err := r.opener.Open(ctx, repo.Path)
	if err != nil {
		return nil, internalError(fmt.Sprintf("Database.%s", field), err)
	}
	return sess, nil
}