M README.md => README.md +17 -7
@@ 255,7 255,7 @@ Once the service is up and you are logged into meta:
```
`--user` selects HTTP Basic auth; the password comes from
- `DOLT_REMOTE_PASSWORD`. A PAT with the `dolt.sr.ht/repos:RW` grant (or an
+ `DOLT_REMOTE_PASSWORD`. A PAT with the `dolt.sr.ht/DATABASES:RW` grant (or an
empty-grant personal token) can push; `:RO` or no grant can only read.
3. **Or clone/push with a dolt keypair (the git-SSH-key-like UX):**
@@ 315,7 315,7 @@ flow). Two tokens are accepted. A **tokens.sr.ht working token** must carry the
a read — and it works only on an instance that configures `[tokens.sr.ht]
origin`; without that section there is no daemon to verify it against, so it is
refused with 401 while everything else keeps working. A **meta personal access
-token** is accepted with the same `dolt.sr.ht/repos:RO` scoping the clone path
+token** is accepted with the same `dolt.sr.ht/DATABASES:RO` scoping the clone path
applies. No credential at all is a normal caller: it reads what an anonymous
visitor reads, and a PRIVATE database it may not see is "not found" rather than
"forbidden", exactly as in the web UI.
@@ 342,7 342,7 @@ There are no mutations; creating, renaming and deleting a database stay behind
the web UI.
The credential plane is `/mcp`'s exactly — a meta personal access token scoped
-`dolt.sr.ht/repos:RO`, or a tokens.sr.ht working token carrying `dolt:read` —
+`dolt.sr.ht/DATABASES:RO`, or a tokens.sr.ht working token carrying `dolt:read` —
and no credential at all is a normal caller that reads what an anonymous visitor
reads. A database the caller may not see resolves to `null` rather than to an
authorization error, so its existence cannot be read out of the shape of the
@@ 353,10 353,20 @@ Listings page with the instance-standard opaque cursor:
`databases(cursor: "…")` continues the walk.
Beside it, `/query/api-meta.json` publishes the one grant this service defines
-(`repos`), which is what lets meta.sr.ht offer `dolt.sr.ht/repos:RO` on its
-personal-token page. To federate the schema into `api.sr.ht`, give the gateway's
-config a `[dolt.sr.ht] api-origin=` line pointing here and SIGHUP it; nothing in
-this service depends on the gateway existing.
+(`DATABASES`), which is what lets meta.sr.ht offer `dolt.sr.ht/DATABASES:RO` on
+its personal-token page. Upper case is not decoration: meta discovers the list
+once at startup and then validates a requested grant against it verbatim, so the
+name a user types has to match character for character — and every other service
+on the instance names its scopes that way. **Deploying a change to it is
+two-sided**: this service ships the new name, and meta.sr.ht has to be restarted
+before anyone can mint a token carrying it.
+
+To federate the schema into `api.sr.ht`, give the gateway's config a
+`[dolt.sr.ht] api-origin=` line pointing here and SIGHUP it; nothing in this
+service depends on the gateway existing. The gateway does not read this file —
+it forwards the client's `Authorization` header to each service and lets the
+service decide — so the grant name concerns meta.sr.ht and hand-written grants,
+not federation.
**`hut graphql dolt` needs a patched hut.** Upstream v0.8.0 carries a hard-coded
list of the ten sr.ht services and dereferences a nil entry for anything else,
M authn/bearer.go => authn/bearer.go +4 -4
@@ 29,7 29,7 @@ const bearerScheme = "Bearer"
// ErrMissingGrant is the sentinel wrapped by every "the credential is good, it
// does not cover this" refusal: a working token without core.GrantRead, or a
-// meta PAT whose OAuth grants do not reach dolt.sr.ht repositories. It is a 403
+// meta PAT whose OAuth grants do not reach this service's databases. It is a 403
// — retrying with the same token is pointless, and the holder needs to be told
// to ask for a wider grant rather than to authenticate again.
//
@@ 231,7 231,7 @@ func resolveWorkingToken(ctx context.Context, v InstanceValidator, presented str
ac.AuthMethod = AuthMethodInstanceToken
// BearerToken and Grants are deliberately left unset. They are core-go's
// OAuth fields, and filling them would subject this caller to
- // TokenGrantsAllow — a gate demanding "dolt.sr.ht/repos:RO", which a
+ // TokenGrantsAllow — a gate demanding "dolt.sr.ht/DATABASES:RO", which a
// tokens.sr.ht grant string can never spell. A working token is scoped by
// its own vocabulary, on BearerCaller.Grants, and by core.Allowed.
@@ 260,8 260,8 @@ func resolveMetaPAT(ctx context.Context, username, presented string) (*BearerCal
// The whole surface is a read, so the gate can be applied once here rather
// than per action. It is the same check the clone path applies.
if !TokenGrantsAllow(ac, core.AccessRO) {
- return nil, fmt.Errorf("%w: %w: token grants do not permit %s on %s repositories",
- ErrInvalidToken, ErrMissingGrant, core.AccessRO, RepoScope)
+ return nil, fmt.Errorf("%w: %w: token grants do not permit %s under %s",
+ ErrInvalidToken, ErrMissingGrant, core.AccessRO, DatabaseScope)
}
return &BearerCaller{AuthContext: ac, InstanceToken: false}, nil
M authn/bearer_test.go => authn/bearer_test.go +5 -5
@@ 204,7 204,7 @@ func TestResolveBearer_WorkingToken_NoValidatorIsRefused(t *testing.T) {
})
t.Run("a meta PAT still resolves", func(t *testing.T) {
- pat := forgePAT("bigbes", "dolt.sr.ht/repos:RO", time.Now().Add(time.Hour))
+ pat := forgePAT("bigbes", "dolt.sr.ht/DATABASES:RO", time.Now().Add(time.Hour))
bc, err := ResolveBearer(testCtx(), nil, pat)
require.NoError(t, err)
assert.False(t, bc.InstanceToken)
@@ 240,7 240,7 @@ func TestResolveBearer_MetaPAT_SufficientGrants(t *testing.T) {
"bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER),
}})
v := &fakeValidator{err: errBackendDown} // must not be consulted at all
- pat := forgePAT("bigbes", "dolt.sr.ht/repos:RO", time.Now().Add(time.Hour))
+ pat := forgePAT("bigbes", "dolt.sr.ht/DATABASES:RO", time.Now().Add(time.Hour))
bc, err := ResolveBearer(testCtx(), v, pat)
require.NoError(t, err)
@@ 284,7 284,7 @@ func TestResolveBearer_MetaPAT_InsufficientGrants(t *testing.T) {
}
func TestResolveBearer_MetaPAT_Revoked(t *testing.T) {
- pat := forgePAT("bigbes", "dolt.sr.ht/repos:RO", time.Now().Add(time.Hour))
+ pat := forgePAT("bigbes", "dolt.sr.ht/DATABASES:RO", time.Now().Add(time.Hour))
withStubBackend(t, &stubBackend{
users: map[string]auth.AuthContext{"bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER)},
revoked: map[[64]byte]bool{sha512.Sum512([]byte(pat)): true},
@@ 300,7 300,7 @@ func TestResolveBearer_MetaPAT_BackendDownIsTransient(t *testing.T) {
users: map[string]auth.AuthContext{"bigbes": sampleUser(1, "bigbes", auth.USER_TYPE_USER)},
revokeErr: errBackendDown,
})
- pat := forgePAT("bigbes", "dolt.sr.ht/repos:RO", time.Now().Add(time.Hour))
+ pat := forgePAT("bigbes", "dolt.sr.ht/DATABASES:RO", time.Now().Add(time.Hour))
_, err := ResolveBearer(testCtx(), nil, pat)
require.Error(t, err)
@@ 317,7 317,7 @@ func TestResolveBearer_UnusableCredentials(t *testing.T) {
}{
{"empty (the caller lost its own header)", ""},
{"garbage", "this-is-not-a-token"},
- {"expired meta PAT", forgePAT("bigbes", "dolt.sr.ht/repos:RO", time.Now().Add(-time.Minute))},
+ {"expired meta PAT", forgePAT("bigbes", "dolt.sr.ht/DATABASES:RO", time.Now().Add(-time.Minute))},
{"expired working token", forgeWorkingToken("bigbes", time.Now().Add(-time.Minute))},
}
for _, tc := range cases {
M authn/token.go => authn/token.go +25 -7
@@ 12,11 12,25 @@ import (
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
-// RepoScope is the OAuth grant scope a meta.sr.ht personal access token must
-// carry to act on dolt.sr.ht repositories: "dolt.sr.ht/repos". Reads require
-// ":RO", pushes require ":RW". Personal tokens with no explicit grants are
-// universal and pass unconditionally (auth.Grants.HasAll semantics).
-const RepoScope = "dolt.sr.ht/repos"
+// DatabaseScope is the OAuth grant scope a meta.sr.ht personal access token
+// must carry to act on this service's databases: "dolt.sr.ht/DATABASES". Reads
+// require ":RO", pushes require ":RW". Personal tokens with no explicit grants
+// are universal and pass unconditionally (auth.Grants.HasAll semantics).
+//
+// The spelling is not free. meta.sr.ht discovers a service's scopes from its
+// api-meta.json and then validates a requested grant with a plain `scope in
+// scopes` — no case folding, no aliasing — so the string published there and
+// the string checked here must match exactly, and a grant a user types by hand
+// must match too. Every other service on the instance names its scopes in
+// upper case (git.sr.ht/REPOSITORIES, todo.sr.ht/TRACKERS, paste.sr.ht/PASTES),
+// because upstream derives them from a GraphQL enum; this service has no
+// @access directive to derive from, so it follows the convention deliberately.
+//
+// DATABASES rather than REPOS because that is what the surface calls the
+// object everywhere a user meets it — the GraphQL `databases` connection, the
+// web pages, the docs. The storage layer underneath still says "repo"; that is
+// a separate, deeper rename and is not what a token grant names.
+const DatabaseScope = "dolt.sr.ht/DATABASES"
// tokenCacheTTL bounds how long a positively-resolved Basic token is trusted
// without re-checking revocation on meta.sr.ht. A single push issues many RPCs;
@@ 123,7 137,7 @@ func ResolveBasic(ctx context.Context, username, password string) (*auth.AuthCon
// TokenGrantsAllow reports whether the caller's token grants permit access at
// the given mode (core.AccessRO for browse/clone, core.AccessRW for push) on
-// dolt.sr.ht repositories. It is the OAuth-grant gate that complements the ACL
+// this service's databases. It is the OAuth-grant gate that complements the ACL
// decision in core.Allowed: a token must carry BOTH sufficient grants and a
// sufficient ACL/visibility to act.
//
@@ 131,6 145,10 @@ func ResolveBasic(ctx context.Context, username, password string) (*auth.AuthCon
// and are not scoped by them, so they pass this gate unconditionally; their
// access is decided solely by core.Allowed. Personal tokens with empty grants
// are universal and also pass.
+//
+// The match is exact, DatabaseScope and nothing else: auth.Grants.Has is a map
+// lookup, so a token minted against an older spelling of this scope is refused
+// here rather than quietly honoured. Re-minting it is the fix.
func TokenGrantsAllow(ac *auth.AuthContext, mode core.AccessMode) bool {
if ac == nil || ac.BearerToken == nil {
return true
@@ 139,5 157,5 @@ func TokenGrantsAllow(ac *auth.AuthContext, mode core.AccessMode) bool {
if mode == core.AccessRW {
kind = auth.RW
}
- return ac.Grants.Has(RepoScope, kind)
+ return ac.Grants.Has(DatabaseScope, kind)
}
M authn/token_test.go => authn/token_test.go +12 -5
@@ 219,11 219,18 @@ func TestTokenGrantsAllow(t *testing.T) {
{"cookie (no bearer) passes", &auth.AuthContext{AuthMethod: auth.AUTH_COOKIE}, core.AccessRW, true},
{"empty grants RO", patAC(""), core.AccessRO, true},
{"empty grants RW", patAC(""), core.AccessRW, true},
- {"repos:RO allows read", patAC("dolt.sr.ht/repos:RO"), core.AccessRO, true},
- {"repos:RO denies write", patAC("dolt.sr.ht/repos:RO"), core.AccessRW, false},
- {"repos:RW allows read", patAC("dolt.sr.ht/repos:RW"), core.AccessRO, true},
- {"repos:RW allows write", patAC("dolt.sr.ht/repos:RW"), core.AccessRW, true},
- {"unrelated scope denies read", patAC("git.sr.ht/repos:RW"), core.AccessRO, false},
+ {"DATABASES:RO allows read", patAC("dolt.sr.ht/DATABASES:RO"), core.AccessRO, true},
+ {"DATABASES:RO denies write", patAC("dolt.sr.ht/DATABASES:RO"), core.AccessRW, false},
+ {"DATABASES:RW allows read", patAC("dolt.sr.ht/DATABASES:RW"), core.AccessRO, true},
+ {"DATABASES:RW allows write", patAC("dolt.sr.ht/DATABASES:RW"), core.AccessRW, true},
+ {"unrelated scope denies read", patAC("git.sr.ht/REPOSITORIES:RW"), core.AccessRO, false},
+ // The scope was published lower case as "repos" before the instance
+ // convention was followed. auth.Grants.Has is a map lookup, so a token
+ // minted then is refused rather than silently honoured — the two cases
+ // below are what "no backward compatibility" means, asserted rather
+ // than assumed.
+ {"legacy lower-case scope denies read", patAC("dolt.sr.ht/repos:RW"), core.AccessRO, false},
+ {"legacy lower-case scope denies write", patAC("dolt.sr.ht/repos:RW"), core.AccessRW, false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
M cmd/doltsrht/graphql.go => cmd/doltsrht/graphql.go +12 -7
@@ 16,13 16,18 @@ import (
// ecore's apimeta, at apimeta.Path.
const queryRoute = "/query"
-// repoScopeName is the one grant this service defines, spelled as meta.sr.ht
-// expects it: the part after the service name in authn.RepoScope
-// ("dolt.sr.ht/repos"). meta prefixes the service name itself. The two
-// spellings are the same fact written twice, so a test asserts them equal —
-// a drift would let a user mint a token meta calls valid and this service does
-// not honour.
-const repoScopeName = "repos"
+// databaseScopeName is the one grant this service defines, spelled as
+// meta.sr.ht expects it: the part after the service name in
+// authn.DatabaseScope ("dolt.sr.ht/DATABASES"). meta prefixes the service name
+// itself. The two spellings are the same fact written twice, so a test asserts
+// them equal — a drift would let a user mint a token meta calls valid and this
+// service does not honour.
+//
+// meta reads this list once, at import time (metasrht/blueprints/oauth2.py),
+// and validates a requested grant against it verbatim. So changing it is a
+// two-sided deployment: this service ships the new name, and meta.sr.ht has to
+// be restarted before anyone can mint a token carrying it.
+const databaseScopeName = "DATABASES"
// graphBrowseOpener satisfies graph.BrowseOpener over browse.Open, as
// mcpBrowseOpener does for the MCP surface and web.BrowseAdapter for the pages:
M cmd/doltsrht/graphql_test.go => cmd/doltsrht/graphql_test.go +16 -5
@@ 4,6 4,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
+ "strings"
"testing"
"github.com/stretchr/testify/assert"
@@ 16,24 17,34 @@ import (
// The scope this service advertises and the scope it enforces are the same fact
// written twice: meta.sr.ht prefixes the service name to what it reads from
-// api-meta.json, and authn.RepoScope is what a presented token is checked
+// api-meta.json, and authn.DatabaseScope is what a presented token is checked
// against. A drift would let a user mint a token meta calls valid and the clone
// path does not honour, which is a support ticket rather than an error.
func TestTheAdvertisedScopeIsTheEnforcedOne(t *testing.T) {
- assert.Equal(t, authn.RepoScope, serviceName+"/"+repoScopeName)
+ assert.Equal(t, authn.DatabaseScope, serviceName+"/"+databaseScopeName)
+}
+
+// The name is upper case because meta.sr.ht compares it verbatim — no case
+// folding — and every other service on the instance names its scopes that way
+// (git.sr.ht/REPOSITORIES, todo.sr.ht/TRACKERS, paste.sr.ht/PASTES). A lower
+// case scope is accepted by meta and works, which is exactly why the drift
+// survived: it breaks only the user who spells the grant by hand, by analogy
+// with every neighbouring service.
+func TestTheScopeIsSpelledLikeEveryOtherServices(t *testing.T) {
+ assert.Equal(t, strings.ToUpper(databaseScopeName), databaseScopeName)
}
// The wiring, not the package: ecore's apimeta owns the never-null rule, and
// this asserts dolt.sr.ht actually declares the grant it enforces rather than
// serving an empty list that would leave meta with no checkbox to offer.
-func TestAPIMetaAdvertisesTheRepoScope(t *testing.T) {
+func TestAPIMetaAdvertisesTheDatabaseScope(t *testing.T) {
rec := httptest.NewRecorder()
- apimeta.Handler(repoScopeName).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, apimeta.Path, nil))
+ apimeta.Handler(databaseScopeName).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, apimeta.Path, nil))
require.Equal(t, http.StatusOK, rec.Code)
var got apimeta.Meta
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
- assert.Equal(t, []string{"repos"}, got.Scopes)
+ assert.Equal(t, []string{"DATABASES"}, got.Scopes)
assert.NotContains(t, rec.Body.String(), "null")
}
M cmd/doltsrht/main.go => cmd/doltsrht/main.go +1 -1
@@ 355,7 355,7 @@ func mountRoutes(r chi.Router, s surfaces) error {
// The file meta.sr.ht reads to learn what this service can be granted.
// A service that mounts its own /query owes the instance this too —
// core-go serves it only for the schemas it hosts itself.
- r.Get(apimeta.Path, apimeta.Handler(repoScopeName))
+ r.Get(apimeta.Path, apimeta.Handler(databaseScopeName))
})
r.Use(authn.OptionalCookieMiddleware()) // never 401s; anonymous stays anonymous
M cmd/doltsrht/main_test.go => cmd/doltsrht/main_test.go +1 -1
@@ 505,6 505,6 @@ func TestTheAPIMetaFileIsServed(t *testing.T) {
var got apimeta.Meta
require.NoError(t, json.Unmarshal(raw, &got))
- assert.Equal(t, []string{repoScopeName}, got.Scopes)
+ assert.Equal(t, []string{databaseScopeName}, got.Scopes)
assert.NotEmpty(t, got.WebhookPubkey)
}
M config.example.ini => config.example.ini +4 -2
@@ 61,8 61,10 @@ log-level=info
; the credential presented — the bearer plane /mcp defines, or none at all for
; an anonymous caller reading public databases — and by the visibility rules the
; web UI applies. Beside it, /query/api-meta.json tells meta.sr.ht that this
-; service defines the "repos" grant, which is what makes
-; "dolt.sr.ht/repos:RO" offerable on the personal-token page.
+; service defines the "DATABASES" grant, which is what makes
+; "dolt.sr.ht/DATABASES:RO" offerable on the personal-token page. meta reads
+; that file once at startup, so it has to be restarted after this service
+; changes the name — until then it offers the old one and refuses the new.
;
; There is deliberately no mcp-enabled key. The read-only MCP surface an agent
; calls (docs/DESIGN.mcp.md) is served at /mcp on the web listener above,
M docs/DESIGN.md => docs/DESIGN.md +4 -2
@@ 215,8 215,10 @@ Ops: OpBrowse, OpCloneRead, OpPush, OpAdmin.
| owner | all | all | all |
PRIVATE + unauthorized ⇒ NotFound (don't leak existence). Suspended users: reads
-allowed, push/admin denied. Token grants: `dolt.sr.ht/repos:RO` for reads, `:RW` for
-push (empty-grant personal tokens pass via `HasAll`).
+allowed, push/admin denied. Token grants: `dolt.sr.ht/DATABASES:RO` for reads, `:RW` for
+push (empty-grant personal tokens pass via `HasAll`). The grant name is matched
+exactly — `auth.Grants.Has` is a map lookup — so the spelling published in
+`/query/api-meta.json` and the one enforced here are asserted equal by a test.
## remotesapi subsystem
M graph/server.go => graph/server.go +1 -1
@@ 24,7 24,7 @@
// every field applies the same access matrix the web pages and the MCP tools do:
//
// - The credential is the bearer plane /mcp already defines — a meta personal
-// access token scoped `dolt.sr.ht/repos:RO`, or a tokens.sr.ht working token
+// access token scoped `dolt.sr.ht/DATABASES:RO`, or a tokens.sr.ht working token
// carrying `dolt:read`. No cookie: an API client is not a browser, and this
// endpoint is deliberately outside web's same-origin group.
// - Anonymous is a normal caller. It reads what an anonymous visitor reads,
M mcpsrv/http_test.go => mcpsrv/http_test.go +1 -1
@@ 155,7 155,7 @@ func mountMCP(t *testing.T, tokensDaemon bool) (*httptest.Server, credentials) {
aliceWorking: forgeWorkingToken("alice"),
bobWorking: forgeWorkingToken("bob"),
withoutRead: forgeWorkingToken("alice"),
- alicePAT: forgePAT("alice", "dolt.sr.ht/repos:RO"),
+ alicePAT: forgePAT("alice", "dolt.sr.ht/DATABASES:RO"),
foreignPAT: forgePAT("alice", "git.sr.ht/repos:RW"),
unverifiable: forgeWorkingToken("alice"),
garbage: "not-a-token",
M remoteapi/integration_test.go => remoteapi/integration_test.go +3 -3
@@ 200,7 200,7 @@ func TestRemoteAPIIntegration(t *testing.T) {
privURL := fmt.Sprintf("http://%s/~alice/privdb", addr)
roURL := fmt.Sprintf("http://%s/~alice/rodb", addr)
- pat := forgePAT("alice", "dolt.sr.ht/repos:RW", time.Now().Add(time.Hour))
+ pat := forgePAT("alice", "dolt.sr.ht/DATABASES:RW", time.Now().Add(time.Hour))
// (1) Anonymous clone of PUBLIC succeeds.
t.Run("anonymous_public_clone", func(t *testing.T) {
@@ 324,7 324,7 @@ func TestRemoteAPIIntegration(t *testing.T) {
// (7) ACL RO grantee (bob) can clone the private rodb but cannot push.
t.Run("acl_ro_clone_not_push", func(t *testing.T) {
- bobPAT := forgePAT("bob", "dolt.sr.ht/repos:RW", time.Now().Add(time.Hour))
+ bobPAT := forgePAT("bob", "dolt.sr.ht/DATABASES:RW", time.Now().Add(time.Hour))
env := append(append([]string{}, baseEnv...), "DOLT_REMOTE_PASSWORD="+bobPAT)
dir := t.TempDir()
@@ 387,7 387,7 @@ func TestRemoteAPIIntegration(t *testing.T) {
// owner-only guard, which also avoids leaking a stranger's namespace.
t.Run("push_to_create_foreign_namespace_denied", func(t *testing.T) {
foreignURL := fmt.Sprintf("http://%s/~alice/bobtried", addr)
- bobPAT := forgePAT("bob", "dolt.sr.ht/repos:RW", time.Now().Add(time.Hour))
+ bobPAT := forgePAT("bob", "dolt.sr.ht/DATABASES:RW", time.Now().Add(time.Hour))
env := append(append([]string{}, baseEnv...), "DOLT_REMOTE_PASSWORD="+bobPAT)
work := filepath.Join(t.TempDir(), "bobtried")
M remoteapi/interceptors.go => remoteapi/interceptors.go +1 -1
@@ 205,7 205,7 @@ func (i *interceptor) authenticate(ctx context.Context) (*auth.AuthContext, erro
//
// 1. classify the method (unknown ⇒ PermissionDenied);
// 2. OAuth grant gate (TokenGrantsAllow) — PAT callers must carry
-// dolt.sr.ht/repos:RO for reads / :RW for pushes; anonymous, cookie and
+// dolt.sr.ht/DATABASES:RO for reads / :RW for pushes; anonymous, cookie and
// dolt-key callers pass trivially;
// 3. extract the repo path (Root with no path ⇒ unauthenticated-OK ping);
// 4. load the repository row and the caller's effective ACL;
M remoteapi/interceptors_test.go => remoteapi/interceptors_test.go +3 -3
@@ 299,20 299,20 @@ func TestAuthorizeGrantGate(t *testing.T) {
t.Run("PAT with RO grant allowed on read of own repo", func(t *testing.T) {
i := testInterceptor(&stubStore{repo: repo(1, 42, core.VisibilityPublic)})
- if _, err := i.authorize(ctx, patCaller(t, 42, "dolt.sr.ht/repos:RO"), mGetMeta, req); err != nil {
+ if _, err := i.authorize(ctx, patCaller(t, 42, "dolt.sr.ht/DATABASES:RO"), mGetMeta, req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("PAT with RO grant cannot push", func(t *testing.T) {
i := testInterceptor(&stubStore{repo: repo(1, 42, core.VisibilityPublic)})
- _, err := i.authorize(ctx, patCaller(t, 42, "dolt.sr.ht/repos:RO"), mCommit, req)
+ _, err := i.authorize(ctx, patCaller(t, 42, "dolt.sr.ht/DATABASES:RO"), mCommit, req)
wantCode(t, err, codes.PermissionDenied)
})
t.Run("PAT with RW grant pushes own repo", func(t *testing.T) {
i := testInterceptor(&stubStore{repo: repo(1, 42, core.VisibilityPublic)})
- if _, err := i.authorize(ctx, patCaller(t, 42, "dolt.sr.ht/repos:RW"), mCommit, req); err != nil {
+ if _, err := i.authorize(ctx, patCaller(t, 42, "dolt.sr.ht/DATABASES:RW"), mCommit, req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})