package remoteapi
import (
"context"
"testing"
"sourcecraft.dev/bigbes/sr-ht-core/auth"
"sourcecraft.dev/bigbes/sr-ht-core/config"
remotesapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/remotesapi/v1alpha1"
"github.com/sirupsen/logrus"
"github.com/vaughan0/go-ini"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"sourcecraft.dev/bigbes/sr-ht-dolt/authn"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
"sourcecraft.dev/bigbes/sr-ht-dolt/db"
)
const (
mRoot = "/dolt.services.remotesapi.v1alpha1.ChunkStoreService/Root"
mGetMeta = "/dolt.services.remotesapi.v1alpha1.ChunkStoreService/GetRepoMetadata"
mCommit = "/dolt.services.remotesapi.v1alpha1.ChunkStoreService/Commit"
mUpload = "/dolt.services.remotesapi.v1alpha1.ChunkStoreService/GetUploadLocations"
mDownload = "/dolt.services.remotesapi.v1alpha1.ChunkStoreService/GetDownloadLocations"
mUnknown = "/dolt.services.remotesapi.v1alpha1.ChunkStoreService/Frobnicate"
)
// fakeReq implements repoRequest with a fixed path/id so tests need not depend
// on which concrete proto message carries the repo for a given method.
type fakeReq struct {
path string
id *remotesapi.RepoId
}
func (f fakeReq) GetRepoPath() string { return f.path }
func (f fakeReq) GetRepoId() *remotesapi.RepoId { return f.id }
// stubStore is an in-memory repoStore: no Postgres.
type stubStore struct {
repo *core.Repo
repoErr error
acl *core.AccessMode
aclErr error
}
func (s stubStore) GetRepoByOwnerAndName(_ context.Context, _, _ string) (*core.Repo, error) {
if s.repoErr != nil {
return nil, s.repoErr
}
return s.repo, nil
}
func (s stubStore) EffectiveAccess(_ context.Context, _, _ int) (*core.AccessMode, error) {
return s.acl, s.aclErr
}
func testInterceptor(store repoStore) *interceptor {
return &interceptor{
conf: ini.File{},
service: "dolt.sr.ht",
expectedAud: "dolt.srht.bigb.es",
logger: logrus.NewEntry(logrus.New()),
stores: func() repoStore { return store },
}
}
func repo(id, ownerID int, vis core.Visibility) *core.Repo {
return &core.Repo{ID: id, Name: "db", OwnerID: ownerID, OwnerName: "alice", Visibility: vis}
}
func acl(m core.AccessMode) *core.AccessMode { return &m }
// patCaller builds an authenticated PAT caller with the given grant string
// (empty ⇒ universal token). config context is needed by DecodeGrants.
func patCaller(t *testing.T, userID int, grants string) *auth.AuthContext {
t.Helper()
ctx := config.Context(context.Background(), ini.File{}, "dolt.sr.ht")
g, err := auth.DecodeGrants(ctx, grants)
if err != nil {
t.Fatalf("DecodeGrants(%q): %v", grants, err)
}
return &auth.AuthContext{
UserID: userID,
Username: "alice",
UserType: auth.USER_TYPE_USER,
AuthMethod: auth.AUTH_OAUTH2,
BearerToken: &auth.BearerToken{},
Grants: g,
}
}
// cookieCaller builds an authenticated non-token caller (cookie/keypair): no
// BearerToken, so the grant gate passes trivially.
func cookieCaller(userID int) *auth.AuthContext {
return &auth.AuthContext{
UserID: userID,
Username: "alice",
UserType: auth.USER_TYPE_USER,
AuthMethod: auth.AUTH_COOKIE,
}
}
func wantCode(t *testing.T, err error, code codes.Code) {
t.Helper()
if status.Code(err) != code {
t.Fatalf("want gRPC code %s, got %s (err=%v)", code, status.Code(err), err)
}
}
func TestClassify(t *testing.T) {
cases := []struct {
method string
op core.Op
mode core.AccessMode
ok bool
}{
{mCommit, core.OpPush, core.AccessRW, true},
{mUpload, core.OpPush, core.AccessRW, true},
{mRoot, core.OpCloneRead, core.AccessRO, true},
{mGetMeta, core.OpCloneRead, core.AccessRO, true},
{mDownload, core.OpCloneRead, core.AccessRO, true},
{mUnknown, 0, "", false},
}
for _, c := range cases {
op, mode, ok := classify(c.method)
if ok != c.ok || (ok && (op != c.op || mode != c.mode)) {
t.Errorf("classify(%s) = (%v,%v,%v), want (%v,%v,%v)",
c.method, op, mode, ok, c.op, c.mode, c.ok)
}
}
}
func TestAuthorize(t *testing.T) {
ctx := context.Background()
req := fakeReq{path: "~alice/db"}
t.Run("anonymous public read allowed", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 100, core.VisibilityPublic)})
if _, err := i.authorize(ctx, nil, mGetMeta, req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("anonymous public push denied (visible)", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 100, core.VisibilityPublic)})
_, err := i.authorize(ctx, nil, mCommit, req)
wantCode(t, err, codes.PermissionDenied)
})
t.Run("anonymous private read is not found", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 100, core.VisibilityPrivate)})
_, err := i.authorize(ctx, nil, mGetMeta, req)
wantCode(t, err, codes.NotFound)
})
t.Run("owner push allowed", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 42, core.VisibilityPrivate)})
if _, err := i.authorize(ctx, cookieCaller(42), mCommit, req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("non-owner private read no acl is not found", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 42, core.VisibilityPrivate)})
_, err := i.authorize(ctx, cookieCaller(7), mGetMeta, req)
wantCode(t, err, codes.NotFound)
})
t.Run("acl RO reads private", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 42, core.VisibilityPrivate), acl: acl(core.AccessRO)})
if _, err := i.authorize(ctx, cookieCaller(7), mGetMeta, req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("acl RO cannot push private (visible ⇒ PermissionDenied)", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 42, core.VisibilityPrivate), acl: acl(core.AccessRO)})
_, err := i.authorize(ctx, cookieCaller(7), mCommit, req)
wantCode(t, err, codes.PermissionDenied)
})
t.Run("acl RW pushes private", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 42, core.VisibilityPrivate), acl: acl(core.AccessRW)})
if _, err := i.authorize(ctx, cookieCaller(7), mCommit, req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("unknown method denied", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 100, core.VisibilityPublic)})
_, err := i.authorize(ctx, nil, mUnknown, req)
wantCode(t, err, codes.PermissionDenied)
})
t.Run("malformed path is invalid argument", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 100, core.VisibilityPublic)})
_, err := i.authorize(ctx, nil, mGetMeta, fakeReq{path: "no-slash"})
wantCode(t, err, codes.InvalidArgument)
})
t.Run("missing repo row is not found", func(t *testing.T) {
i := testInterceptor(stubStore{repoErr: db.ErrNotFound})
_, err := i.authorize(ctx, nil, mGetMeta, req)
wantCode(t, err, codes.NotFound)
})
t.Run("root ping with no path is allowed", func(t *testing.T) {
i := testInterceptor(stubStore{})
got, err := i.authorize(ctx, nil, mRoot, fakeReq{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got == nil {
t.Fatal("expected a caller context")
}
})
t.Run("non-root with no path is invalid argument", func(t *testing.T) {
i := testInterceptor(stubStore{})
_, err := i.authorize(ctx, nil, mGetMeta, fakeReq{})
wantCode(t, err, codes.InvalidArgument)
})
t.Run("repo id shape resolves path", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 100, core.VisibilityPublic)})
r := fakeReq{id: &remotesapi.RepoId{Org: "alice", RepoName: "db"}}
if _, err := i.authorize(ctx, nil, mGetMeta, r); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
func TestAuthorizeGrantGate(t *testing.T) {
ctx := context.Background()
req := fakeReq{path: "~alice/db"}
t.Run("PAT without RO grant denied on read", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 42, core.VisibilityPublic)})
_, err := i.authorize(ctx, patCaller(t, 42, "meta.sr.ht/profile:RO"), mGetMeta, req)
wantCode(t, err, codes.PermissionDenied)
})
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 {
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)
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 {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("universal PAT (empty grants) passes gate", func(t *testing.T) {
i := testInterceptor(stubStore{repo: repo(1, 42, core.VisibilityPublic)})
if _, err := i.authorize(ctx, patCaller(t, 42, ""), mCommit, req); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
// TestUnaryAnonymous exercises the full unary path (authenticate + authorize)
// for an anonymous request: no metadata ⇒ anonymous caller, no crypto/PG.
func TestUnaryAnonymous(t *testing.T) {
i := &interceptor{
conf: ini.File{},
service: "dolt.sr.ht",
logger: logrus.NewEntry(logrus.New()),
stores: func() repoStore { return stubStore{repo: repo(1, 100, core.VisibilityPublic)} },
}
var gotCaller bool
handler := func(ctx context.Context, _ any) (any, error) {
// The handler must see a caller-carrying context (anonymous ⇒ nil ac).
gotCaller = authn.CallerFromContext(ctx) == nil
return "ok", nil
}
info := &grpc.UnaryServerInfo{FullMethod: mGetMeta}
resp, err := i.unary()(context.Background(), fakeReq{path: "~alice/db"}, info, handler)
if err != nil {
t.Fatalf("unary: %v", err)
}
if resp != "ok" || !gotCaller {
t.Fatalf("handler not invoked with anonymous caller context (resp=%v)", resp)
}
}
func TestNormalizeAud(t *testing.T) {
cases := map[string]string{
"dolt.srht.bigb.es": "dolt.srht.bigb.es",
"dolt.srht.bigb.es:443": "dolt.srht.bigb.es",
"127.0.0.1:5306": "127.0.0.1",
"": "",
}
for in, want := range cases {
if got := normalizeAud(in); got != want {
t.Errorf("normalizeAud(%q) = %q, want %q", in, got, want)
}
}
}