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) } } }