From 704b36a143496487829af708e657da02c9085f01 Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Mon, 27 May 2024 13:39:57 -0700 Subject: [PATCH] feat/enterprise-portal: ConnectRPC layer for {Get/List}CodyGatewayAccess (#62771) This PR exposes the data layer implemented in https://github.com/sourcegraph/sourcegraph/pull/62706 via the Enterprise Portal API. We register the services proposed in #62263 and also set up tooling like gRPC UI locally for DX. Auth is via SAMS M2M; https://github.com/sourcegraph/sourcegraph-accounts-sdk-go/pull/28 and https://github.com/sourcegraph/sourcegraph-accounts/pull/227 rolls out the new scopes, and https://github.com/sourcegraph/managed-services/pull/1474 adds credentials for the enterprise-portal-dev deployment. Closes CORE-112 ## Test plan https://github.com/sourcegraph/sourcegraph/pull/62706 has extensive testing of the data layer, and this PR expands on it a little bit. I tested the RPC layer by hand: Create SAMS client for Enterprise Portal Dev in **accounts.sgdev.org**: ```sh curl -s -X POST \ -H "Authorization: Bearer $MANAGEMENT_SECRET" \ https://accounts.sgdev.org/api/management/v1/identity-provider/clients \ --data '{"name": "enterprise-portal-dev", "scopes": [], "redirect_uris": ["https://enterprise-portal.sgdev.org"]}' | jq ``` Configure `sg.config.overwrite.yaml` ```yaml enterprise-portal: env: SRC_LOG_LEVEL: debug # sams-dev SAMS_URL: https://accounts.sgdev.org ENTERPRISE_PORTAL_SAMS_CLIENT_ID: "sams_cid_..." ENTERPRISE_PORTAL_SAMS_CLIENT_SECRET: "sams_cs_..." ``` Create a test client (later, we will do the same thing for Cody Gateway), also in **accounts.sgdev.org**: ```sh curl -s -X POST \ -H "Authorization: Bearer $MANAGEMENT_SECRET" \ https://accounts.sgdev.org/api/management/v1/identity-provider/clients \ --data '{"name": "enterprise-portal-dev-reader", "scopes": ["enterprise_portal::codyaccess::read", "enterprise_portal::subscription::read"], "redirect_uris": ["https://enterprise-portal.sgdev.org"]}' | jq ``` Then: ``` sg run enterprise-portal ``` Navigate to the locally-enabled gRPC debug UI at http://localhost:6081/debug/grcpui, using https://github.com/sourcegraph/sourcegraph/pull/62883 to get an access token from our test client to add in the request metadata: ```sh sg sams create-client-token -s 'enterprise_portal::codyaccess::read' ``` I'm using some local subscriptions I've made previously in `sg start dotcom`: ![image](https://github.com/sourcegraph/sourcegraph/assets/23356519/a55c6f0d-b0ae-4e68-8e4c-ccb6e2cc442d) ![image](https://github.com/sourcegraph/sourcegraph/assets/23356519/19d18104-1051-4a82-abe0-58010dd13a27) Without a valid authorization header: ![image](https://github.com/sourcegraph/sourcegraph/assets/23356519/c9cf4c89-9902-48f8-ac41-daf9a63ca789) Verified a lookup using the returned access tokens also works --------- Co-authored-by: Jean-Hadrien Chabran Co-authored-by: Joe Chen --- .../internal/codyaccessservice/BUILD.bazel | 25 ++++ .../internal/codyaccessservice/adapters.go | 54 ++++++++ .../internal/codyaccessservice/v1.go | 123 ++++++++++++++++++ .../internal/connectutil/BUILD.bazel | 16 +++ .../internal/connectutil/connectutil.go | 30 +++++ .../internal/dotcomdb/BUILD.bazel | 6 +- .../internal/dotcomdb/dotcomdb.go | 10 +- .../internal/dotcomdb/dotcomdb_test.go | 26 ++++ .../internal/samsm2m/BUILD.bazel | 37 ++++++ .../internal/samsm2m/samsm2m.go | 99 ++++++++++++++ .../internal/samsm2m/samsm2m_test.go | 108 +++++++++++++++ .../internal/subscriptionsservice/BUILD.bazel | 12 ++ .../internal/subscriptionsservice/v1.go | 25 ++++ cmd/enterprise-portal/service/BUILD.bazel | 9 ++ cmd/enterprise-portal/service/config.go | 15 +++ cmd/enterprise-portal/service/service.go | 92 ++++++++++++- .../dotcomproductsubscriptiontest.go | 17 +++ .../codygateway_dotcom_user.go | 2 +- .../dotcom/productsubscription/tokens_db.go | 4 +- .../productsubscription/tokens_db_test.go | 6 +- .../productsubscription/tokens_graphql.go | 2 +- deps.bzl | 28 ++-- go.mod | 13 +- go.sum | 22 ++-- internal/debugserver/grpcui.go | 9 +- .../codyaccess/v1/codyaccess.proto | 3 +- .../subscriptions/v1/BUILD.bazel | 1 + .../subscriptions/v1/subscriptions.proto | 3 +- lib/enterpriseportal/subscriptions/v1/v1.go | 7 + sg.config.yaml | 8 ++ 30 files changed, 765 insertions(+), 47 deletions(-) create mode 100644 cmd/enterprise-portal/internal/codyaccessservice/BUILD.bazel create mode 100644 cmd/enterprise-portal/internal/codyaccessservice/adapters.go create mode 100644 cmd/enterprise-portal/internal/codyaccessservice/v1.go create mode 100644 cmd/enterprise-portal/internal/connectutil/BUILD.bazel create mode 100644 cmd/enterprise-portal/internal/connectutil/connectutil.go create mode 100644 cmd/enterprise-portal/internal/samsm2m/BUILD.bazel create mode 100644 cmd/enterprise-portal/internal/samsm2m/samsm2m.go create mode 100644 cmd/enterprise-portal/internal/samsm2m/samsm2m_test.go create mode 100644 cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel create mode 100644 cmd/enterprise-portal/internal/subscriptionsservice/v1.go create mode 100644 lib/enterpriseportal/subscriptions/v1/v1.go diff --git a/cmd/enterprise-portal/internal/codyaccessservice/BUILD.bazel b/cmd/enterprise-portal/internal/codyaccessservice/BUILD.bazel new file mode 100644 index 00000000000..2d020a192cd --- /dev/null +++ b/cmd/enterprise-portal/internal/codyaccessservice/BUILD.bazel @@ -0,0 +1,25 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "codyaccessservice", + srcs = [ + "adapters.go", + "v1.go", + ], + importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/codyaccessservice", + visibility = ["//cmd/enterprise-portal:__subpackages__"], + deps = [ + "//cmd/enterprise-portal/internal/connectutil", + "//cmd/enterprise-portal/internal/dotcomdb", + "//cmd/enterprise-portal/internal/samsm2m", + "//internal/trace", + "//lib/enterpriseportal/codyaccess/v1:codyaccess", + "//lib/enterpriseportal/codyaccess/v1/v1connect", + "//lib/enterpriseportal/subscriptions/v1:subscriptions", + "//lib/errors", + "@com_connectrpc_connect//:connect", + "@com_github_sourcegraph_log//:log", + "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", + "@org_golang_google_protobuf//types/known/durationpb", + ], +) diff --git a/cmd/enterprise-portal/internal/codyaccessservice/adapters.go b/cmd/enterprise-portal/internal/codyaccessservice/adapters.go new file mode 100644 index 00000000000..f208c46080e --- /dev/null +++ b/cmd/enterprise-portal/internal/codyaccessservice/adapters.go @@ -0,0 +1,54 @@ +package codyaccessservice + +import ( + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/dotcomdb" + codyaccessv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1" + subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1" +) + +func convertAccessAttrsToProto(attrs *dotcomdb.CodyGatewayAccessAttributes) *codyaccessv1.CodyGatewayAccess { + // Provide ID in prefixed format. + subscriptionID := subscriptionsv1.EnterpriseSubscriptionIDPrefix + attrs.SubscriptionID + + // If not enabled, return a minimal response. + if !attrs.CodyGatewayEnabled { + return &codyaccessv1.CodyGatewayAccess{ + SubscriptionId: subscriptionID, + Enabled: false, + } + } + + // If enabled, return the full response. + limits := attrs.EvaluateRateLimits() + return &codyaccessv1.CodyGatewayAccess{ + SubscriptionId: subscriptionID, + Enabled: attrs.CodyGatewayEnabled, + ChatCompletionsRateLimit: &codyaccessv1.CodyGatewayRateLimit{ + Source: limits.ChatSource, + Limit: limits.Chat.Limit, + IntervalDuration: durationpb.New(limits.Chat.IntervalDuration()), + }, + CodeCompletionsRateLimit: &codyaccessv1.CodyGatewayRateLimit{ + Source: limits.CodeSource, + Limit: limits.Code.Limit, + IntervalDuration: durationpb.New(limits.Code.IntervalDuration()), + }, + EmbeddingsRateLimit: &codyaccessv1.CodyGatewayRateLimit{ + Source: limits.EmbeddingsSource, + Limit: limits.Embeddings.Limit, + IntervalDuration: durationpb.New(limits.Embeddings.IntervalDuration()), + }, + AccessTokens: func() []*codyaccessv1.CodyGatewayAccessToken { + accessTokens := attrs.GenerateAccessTokens() + results := make([]*codyaccessv1.CodyGatewayAccessToken, len(accessTokens)) + for i, token := range accessTokens { + results[i] = &codyaccessv1.CodyGatewayAccessToken{ + Token: token, + } + } + return results + }(), + } +} diff --git a/cmd/enterprise-portal/internal/codyaccessservice/v1.go b/cmd/enterprise-portal/internal/codyaccessservice/v1.go new file mode 100644 index 00000000000..66c977d9833 --- /dev/null +++ b/cmd/enterprise-portal/internal/codyaccessservice/v1.go @@ -0,0 +1,123 @@ +package codyaccessservice + +import ( + "context" + "fmt" + "net/http" + + "connectrpc.com/connect" + "github.com/sourcegraph/log" + + "github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes" + + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/connectutil" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/dotcomdb" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/samsm2m" + "github.com/sourcegraph/sourcegraph/internal/trace" + codyaccessv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1" + codyaccessv1connect "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1/v1connect" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const Name = codyaccessv1connect.CodyAccessServiceName + +type DotComDB interface { + GetCodyGatewayAccessAttributesBySubscription(ctx context.Context, subscriptionID string) (*dotcomdb.CodyGatewayAccessAttributes, error) + GetCodyGatewayAccessAttributesByAccessToken(ctx context.Context, subscriptionID string) (*dotcomdb.CodyGatewayAccessAttributes, error) + GetAllCodyGatewayAccessAttributes(ctx context.Context) ([]*dotcomdb.CodyGatewayAccessAttributes, error) +} + +func RegisterV1(logger log.Logger, mux *http.ServeMux, samsClient samsm2m.TokenIntrospector, dotcom DotComDB) { + mux.Handle(codyaccessv1connect.NewCodyAccessServiceHandler(&handlerV1{ + logger: logger.Scoped("codyaccess.v1"), + samsClient: samsClient, + dotcom: dotcom, + })) +} + +type handlerV1 struct { + codyaccessv1connect.UnimplementedCodyAccessServiceHandler + logger log.Logger + + samsClient samsm2m.TokenIntrospector + dotcom DotComDB +} + +var _ codyaccessv1connect.CodyAccessServiceHandler = (*handlerV1)(nil) + +func (s *handlerV1) GetCodyGatewayAccess(ctx context.Context, req *connect.Request[codyaccessv1.GetCodyGatewayAccessRequest]) (*connect.Response[codyaccessv1.GetCodyGatewayAccessResponse], error) { + logger := trace.Logger(ctx, s.logger). + With(log.String("queryType", fmt.Sprintf("%T", req.Msg.GetQuery()))) + + // 🚨 SECURITY: Require approrpiate M2M scope. + requiredScope := samsm2m.EnterprisePortalScope("codyaccess", scopes.ActionRead) + if err := samsm2m.RequireScope(ctx, logger, s.samsClient, requiredScope, req); err != nil { + return nil, err + } + + var attr *dotcomdb.CodyGatewayAccessAttributes + var err error + switch query := req.Msg.GetQuery().(type) { + case *codyaccessv1.GetCodyGatewayAccessRequest_SubscriptionId: + if len(query.SubscriptionId) == 0 { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid query: subscription ID")) + } + attr, err = s.dotcom.GetCodyGatewayAccessAttributesBySubscription(ctx, query.SubscriptionId) + + case *codyaccessv1.GetCodyGatewayAccessRequest_AccessToken: + if len(query.AccessToken) == 0 { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid query: access token")) + } + attr, err = s.dotcom.GetCodyGatewayAccessAttributesByAccessToken(ctx, query.AccessToken) + + default: + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid query")) + } + if err != nil { + if err == dotcomdb.ErrCodyGatewayAccessNotFound { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connectutil.InternalError(ctx, logger, err, + "failed to get Cody Gateway access attributes") + } + return connect.NewResponse(&codyaccessv1.GetCodyGatewayAccessResponse{ + Access: convertAccessAttrsToProto(attr), + }), nil +} + +func (s *handlerV1) ListCodyGatewayAccesses(ctx context.Context, req *connect.Request[codyaccessv1.ListCodyGatewayAccessesRequest]) (*connect.Response[codyaccessv1.ListCodyGatewayAccessesResponse], error) { + logger := trace.Logger(ctx, s.logger) + + // 🚨 SECURITY: Require approrpiate M2M scope. + requiredScope := samsm2m.EnterprisePortalScope("codyaccess", scopes.ActionRead) + if err := samsm2m.RequireScope(ctx, logger, s.samsClient, requiredScope, req); err != nil { + return nil, err + } + + // Pagination is unimplemented: https://linear.app/sourcegraph/issue/CORE-134 + if req.Msg.PageSize != 0 { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("pagination not implemented")) + } + if req.Msg.PageToken != "" { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("pagination not implemented")) + } + + attrs, err := s.dotcom.GetAllCodyGatewayAccessAttributes(ctx) + if err != nil { + if err == dotcomdb.ErrCodyGatewayAccessNotFound { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connectutil.InternalError(ctx, logger, err, + "failed to list Cody Gateway access attributes") + } + resp := codyaccessv1.ListCodyGatewayAccessesResponse{ + // Never a next page, pagination is not implemented yet: + // https://linear.app/sourcegraph/issue/CORE-134 + NextPageToken: "", + Accesses: make([]*codyaccessv1.CodyGatewayAccess, len(attrs)), + } + for i, attr := range attrs { + resp.Accesses[i] = convertAccessAttrsToProto(attr) + } + return connect.NewResponse(&resp), nil +} diff --git a/cmd/enterprise-portal/internal/connectutil/BUILD.bazel b/cmd/enterprise-portal/internal/connectutil/BUILD.bazel new file mode 100644 index 00000000000..78ca3995fe2 --- /dev/null +++ b/cmd/enterprise-portal/internal/connectutil/BUILD.bazel @@ -0,0 +1,16 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "connectutil", + srcs = ["connectutil.go"], + importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/connectutil", + visibility = ["//cmd/enterprise-portal:__subpackages__"], + deps = [ + "//internal/trace", + "//lib/errors", + "@com_connectrpc_connect//:connect", + "@com_github_sourcegraph_log//:log", + "@io_opentelemetry_go_otel//attribute", + "@io_opentelemetry_go_otel_trace//:trace", + ], +) diff --git a/cmd/enterprise-portal/internal/connectutil/connectutil.go b/cmd/enterprise-portal/internal/connectutil/connectutil.go new file mode 100644 index 00000000000..c0682f802ef --- /dev/null +++ b/cmd/enterprise-portal/internal/connectutil/connectutil.go @@ -0,0 +1,30 @@ +package connectutil + +import ( + "context" + + "connectrpc.com/connect" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/sourcegraph/log" + + sgtrace "github.com/sourcegraph/sourcegraph/internal/trace" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// InternalError logs an error, adds it to the trace, and returns a connect +// error with a safe message. +func InternalError(ctx context.Context, logger log.Logger, err error, safeMsg string) error { + trace.SpanFromContext(ctx). + SetAttributes( + attribute.String("full_error", err.Error()), + ) + sgtrace.Logger(ctx, logger). + AddCallerSkip(1). + Error(safeMsg, + log.String("code", connect.CodeInternal.String()), + log.Error(err), + ) + return connect.NewError(connect.CodeInternal, errors.New(safeMsg)) +} diff --git a/cmd/enterprise-portal/internal/dotcomdb/BUILD.bazel b/cmd/enterprise-portal/internal/dotcomdb/BUILD.bazel index 25be72dd55d..01ff9ed98f4 100644 --- a/cmd/enterprise-portal/internal/dotcomdb/BUILD.bazel +++ b/cmd/enterprise-portal/internal/dotcomdb/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "//internal/licensing", "//internal/productsubscription", "//lib/enterpriseportal/codyaccess/v1:codyaccess", + "//lib/enterpriseportal/subscriptions/v1:subscriptions", "//lib/errors", "@com_github_jackc_pgx_v5//:pgx", ], @@ -19,7 +20,10 @@ go_library( go_test( name = "dotcomdb_test", srcs = ["dotcomdb_test.go"], - tags = ["requires-network"], + tags = [ + TAG_INFRA_CORESERVICES, + "requires-network", + ], deps = [ ":dotcomdb", "//cmd/frontend/dotcomproductsubscriptiontest", diff --git a/cmd/enterprise-portal/internal/dotcomdb/dotcomdb.go b/cmd/enterprise-portal/internal/dotcomdb/dotcomdb.go index 287a7e2f07b..38691b4eefe 100644 --- a/cmd/enterprise-portal/internal/dotcomdb/dotcomdb.go +++ b/cmd/enterprise-portal/internal/dotcomdb/dotcomdb.go @@ -17,6 +17,7 @@ import ( "github.com/sourcegraph/sourcegraph/internal/licensing" "github.com/sourcegraph/sourcegraph/internal/productsubscription" codyaccessv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/codyaccess/v1" + subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1" "github.com/sourcegraph/sourcegraph/lib/errors" ) @@ -176,8 +177,12 @@ FROM product_subscriptions subscription ) tokens ON tokens.product_subscription_id = subscription.id` clauses := []string{rawClause} + // Add WHERE clause, amending it to include a condition that the subscription + // must not be archived. if conds.whereClause != "" { - clauses = append(clauses, "WHERE "+conds.whereClause) + clauses = append(clauses, "WHERE "+conds.whereClause+" AND subscription.archived_at IS NULL") + } else { + clauses = append(clauses, "WHERE subscription.archived_at IS NULL") } clauses = append(clauses, "GROUP BY subscription.id") // required, after WHERE clause if conds.havingClause != "" { @@ -195,7 +200,8 @@ func (r *Reader) GetCodyGatewayAccessAttributesBySubscription(ctx context.Contex query := newCodyGatewayAccessQuery(queryConditions{ whereClause: "subscription.id = $1", }) - row := r.conn.QueryRow(ctx, query, subscriptionID) + row := r.conn.QueryRow(ctx, query, + strings.TrimPrefix(subscriptionID, subscriptionsv1.EnterpriseSubscriptionIDPrefix)) return scanCodyGatewayAccessAttributes(row) } diff --git a/cmd/enterprise-portal/internal/dotcomdb/dotcomdb_test.go b/cmd/enterprise-portal/internal/dotcomdb/dotcomdb_test.go index e7faeaf86f8..75b5600a699 100644 --- a/cmd/enterprise-portal/internal/dotcomdb/dotcomdb_test.go +++ b/cmd/enterprise-portal/internal/dotcomdb/dotcomdb_test.go @@ -73,6 +73,8 @@ type mockAccess struct { } func setupDBAndInsertMockLicense(t *testing.T, dotcomdb database.DB, info license.Info, cgAccess graphqlbackend.UpdateCodyGatewayAccessInput) mockAccess { + start := time.Now() + ctx := context.Background() subdb := dotcomproductsubscriptiontest.NewSubscriptionsDB(t, dotcomdb) ldb := dotcomproductsubscriptiontest.NewLicensesDB(t, dotcomdb) @@ -92,6 +94,22 @@ func setupDBAndInsertMockLicense(t *testing.T, dotcomdb database.DB, info licens require.NoError(t, err) } + { + // Create a different subscription and license that's archived, + // created at the same time, to ensure we don't use it + u, err := dotcomdb.Users().Create(ctx, database.NewUser{Username: "archived"}) + require.NoError(t, err) + sub, err := subdb.Create(ctx, u.ID, u.Username) + require.NoError(t, err) + _, err = ldb.Create(ctx, sub, t.Name()+"-archived", 2, license.Info{ + CreatedAt: info.CreatedAt, + ExpiresAt: info.ExpiresAt, + }) + require.NoError(t, err) + // Archive the subscription + require.NoError(t, subdb.Archive(ctx, sub)) + } + // Create the subscription we will assert against u, err := dotcomdb.Users().Create(ctx, database.NewUser{Username: "user"}) require.NoError(t, err) @@ -130,6 +148,7 @@ func setupDBAndInsertMockLicense(t *testing.T, dotcomdb database.DB, info licens require.NoError(t, err) } + t.Logf("Setup complete in %s", time.Since(start).String()) return result } @@ -201,6 +220,13 @@ func TestGetCodyGatewayAccessAttributes(t *testing.T) { attr, err := dotcomreader.GetCodyGatewayAccessAttributesByAccessToken(ctx, token) require.NoError(t, err) validateAccessAttributes(t, dotcomdb, mock, attr, tc.info) + + t.Run("compare with dotcom tokens DB", func(t *testing.T) { + subID, err := dotcomproductsubscriptiontest.NewTokensDB(t, dotcomdb). + LookupProductSubscriptionIDByAccessToken(ctx, token) + require.NoError(t, err) + assert.Equal(t, subID, attr.SubscriptionID) + }) }) } }) diff --git a/cmd/enterprise-portal/internal/samsm2m/BUILD.bazel b/cmd/enterprise-portal/internal/samsm2m/BUILD.bazel new file mode 100644 index 00000000000..963d12ff284 --- /dev/null +++ b/cmd/enterprise-portal/internal/samsm2m/BUILD.bazel @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//dev:go_defs.bzl", "go_test") + +go_library( + name = "samsm2m", + srcs = ["samsm2m.go"], + importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/samsm2m", + visibility = ["//cmd/enterprise-portal:__subpackages__"], + deps = [ + "//cmd/enterprise-portal/internal/connectutil", + "//internal/authbearer", + "//lib/errors", + "@com_connectrpc_connect//:connect", + "@com_github_sourcegraph_log//:log", + "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go", + "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", + "@io_opentelemetry_go_otel//:otel", + "@io_opentelemetry_go_otel//attribute", + "@io_opentelemetry_go_otel//codes", + "@io_opentelemetry_go_otel_trace//:trace", + ], +) + +go_test( + name = "samsm2m_test", + srcs = ["samsm2m_test.go"], + embed = [":samsm2m"], + deps = [ + "//lib/errors", + "@com_github_hexops_autogold_v2//:autogold", + "@com_github_sourcegraph_log//logtest", + "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go", + "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/cmd/enterprise-portal/internal/samsm2m/samsm2m.go b/cmd/enterprise-portal/internal/samsm2m/samsm2m.go new file mode 100644 index 00000000000..f2f1a267bad --- /dev/null +++ b/cmd/enterprise-portal/internal/samsm2m/samsm2m.go @@ -0,0 +1,99 @@ +package samsm2m + +import ( + "context" + "net/http" + + "connectrpc.com/connect" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + otelcodes "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + "github.com/sourcegraph/log" + + sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go" + "github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes" + + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/connectutil" + "github.com/sourcegraph/sourcegraph/internal/authbearer" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// EnterprisePortalScope returns the Enterprise Portal service scope for the +// given permission and action. +func EnterprisePortalScope(permission scopes.Permission, action scopes.Action) scopes.Scope { + return scopes.ToScope(scopes.ServiceEnterprisePortal, permission, action) +} + +var tracer = otel.GetTracerProvider().Tracer("telemetry-gateway/samsm2m") + +type TokenIntrospector interface { + IntrospectToken(ctx context.Context, token string) (*sams.IntrospectTokenResponse, error) +} + +type Request interface { + Header() http.Header +} + +// RequireScope ensures the request context has a valid SAMS M2M token +// with requiredScope. It returns a ConnectRPC status error suitable to be +// returned directly from a ConnectRPC implementation. +// +// See: go/sams-m2m +func RequireScope(ctx context.Context, logger log.Logger, tokens TokenIntrospector, requiredScope scopes.Scope, req Request) (err error) { + logger = logger.Scoped("samsm2m") + + var span trace.Span + ctx, span = tracer.Start(ctx, "RequireScope") + defer func() { + if err != nil { + span.RecordError(err) + span.SetStatus(otelcodes.Error, "check failed") + } + span.End() + }() + + var token string + if v := req.Header().Values("authorization"); len(v) == 1 && v[0] != "" { + var err error + token, err = authbearer.ExtractBearerContents(v[0]) + if err != nil { + return connect.NewError(connect.CodeUnauthenticated, + errors.Wrap(err, "invalid authorization header")) + } + } else { + return connect.NewError(connect.CodeUnauthenticated, + errors.New("no authorization header")) + } + + // TODO: as part of go/sams-m2m we need to build out a SDK for SAMS M2M + // consumers that has a recommended short-caching mechanism. Avoid doing it + // for now until we have a concerted effort. + result, err := tokens.IntrospectToken(ctx, token) + if err != nil { + return connectutil.InternalError(ctx, logger, err, "unable to validate token") + } + span.SetAttributes(attribute.String("client_id", result.ClientID)) + + // Active encapsulates whether the token is active, including expiration. + if !result.Active { + // Record detailed error in span, and return an opaque one + span.SetAttributes(attribute.String("full_error", "inactive token")) + return connect.NewError(connect.CodePermissionDenied, errors.New("permission denied")) + } + + // Check for our required scope. + if !result.Scopes.Match(requiredScope) { + // Record detailed error in span and logs + err = errors.Newf("got scopes %+v, required: %+v", result.Scopes, requiredScope) + span.SetAttributes(attribute.String("full_error", err.Error())) + logger.Error("attempt to authenticate using SAMS token without required scope", + log.String("clientID", result.ClientID), + log.Error(err)) + // Return an opaque error + return connect.NewError(connect.CodePermissionDenied, errors.New("insufficient scope")) + } + + return nil +} diff --git a/cmd/enterprise-portal/internal/samsm2m/samsm2m_test.go b/cmd/enterprise-portal/internal/samsm2m/samsm2m_test.go new file mode 100644 index 00000000000..760489a804a --- /dev/null +++ b/cmd/enterprise-portal/internal/samsm2m/samsm2m_test.go @@ -0,0 +1,108 @@ +package samsm2m + +import ( + "context" + "net/http" + "testing" + + "github.com/hexops/autogold/v2" + "github.com/sourcegraph/log/logtest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go" + "github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes" + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +type mockSAMSClient struct { + result *sams.IntrospectTokenResponse + error error +} + +func (m mockSAMSClient) IntrospectToken(context.Context, string) (*sams.IntrospectTokenResponse, error) { + return m.result, m.error +} + +type request map[string]string + +func (r request) Header() http.Header { + h := make(http.Header) + for k, v := range r { + h.Add(k, v) + } + return h +} + +func TestRequireScope(t *testing.T) { + requiredScope := EnterprisePortalScope("codyaccess", scopes.ActionRead) + + for _, tc := range []struct { + name string + metadata map[string]string + samsClient TokenIntrospector + wantErr autogold.Value + }{ + { + name: "no metadata", + metadata: nil, + samsClient: nil, // will not be used + wantErr: autogold.Expect("unauthenticated: no authorization header"), + }, + { + name: "no authorization header", + metadata: map[string]string{"somethingelse": "foobar"}, + samsClient: nil, // will not be used + wantErr: autogold.Expect("unauthenticated: no authorization header"), + }, + { + name: "malformed authorization header", + metadata: map[string]string{"authorization": "bearer"}, + samsClient: nil, // will not be used + wantErr: autogold.Expect("unauthenticated: invalid authorization header: token type missing in Authorization header"), + }, + { + name: "token ok, introspect failed", + metadata: map[string]string{"authorization": "bearer foobar"}, + samsClient: mockSAMSClient{error: errors.New("introspection failed")}, + wantErr: autogold.Expect("internal: unable to validate token"), + }, + { + name: "token ok, but inactive", + metadata: map[string]string{"authorization": "bearer foobar"}, + samsClient: mockSAMSClient{result: &sams.IntrospectTokenResponse{Active: false}}, + wantErr: autogold.Expect("permission_denied: permission denied"), + }, + { + name: "token ok and active, but invalid scope", + metadata: map[string]string{"authorization": "bearer foobar"}, + samsClient: mockSAMSClient{result: &sams.IntrospectTokenResponse{ + Active: true, + Scopes: scopes.ToScopes([]string{"foo", "bar"}), + }}, + wantErr: autogold.Expect("permission_denied: insufficient scope"), + }, + { + name: "token ok and active and valid scope", + metadata: map[string]string{"authorization": "bearer foobar"}, + samsClient: mockSAMSClient{ + result: &sams.IntrospectTokenResponse{ + Active: true, + Scopes: append(scopes.ToScopes([]string{"foo", "bar"}), requiredScope), + }, + }, + wantErr: nil, // success + }, + } { + t.Run(tc.name, func(t *testing.T) { + ctx := context.Background() + err := RequireScope(ctx, logtest.Scoped(t), tc.samsClient, requiredScope, request(tc.metadata)) + if tc.wantErr == nil { + assert.NoError(t, err) + } else { + require.Error(t, err) + tc.wantErr.Equal(t, err.Error()) + } + }) + } +} diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel b/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel new file mode 100644 index 00000000000..b0de4136130 --- /dev/null +++ b/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "subscriptionsservice", + srcs = ["v1.go"], + importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/subscriptionsservice", + visibility = ["//cmd/enterprise-portal:__subpackages__"], + deps = [ + "//lib/enterpriseportal/subscriptions/v1/v1connect", + "@com_github_sourcegraph_log//:log", + ], +) diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/v1.go b/cmd/enterprise-portal/internal/subscriptionsservice/v1.go new file mode 100644 index 00000000000..12ec2aee07f --- /dev/null +++ b/cmd/enterprise-portal/internal/subscriptionsservice/v1.go @@ -0,0 +1,25 @@ +package subscriptionsservice + +import ( + "net/http" + + "github.com/sourcegraph/log" + + subscriptionsv1connect "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1/v1connect" +) + +const Name = subscriptionsv1connect.SubscriptionsServiceName + +func RegisterV1(logger log.Logger, mux *http.ServeMux) { + mux.Handle(subscriptionsv1connect.NewSubscriptionsServiceHandler(&handlerV1{ + logger: logger.Scoped("subscriptions.v1"), + })) +} + +type handlerV1 struct { + subscriptionsv1connect.UnimplementedSubscriptionsServiceHandler + + logger log.Logger +} + +var _ subscriptionsv1connect.SubscriptionsServiceHandler = (*handlerV1)(nil) diff --git a/cmd/enterprise-portal/service/BUILD.bazel b/cmd/enterprise-portal/service/BUILD.bazel index a760e54c0df..db08f84ff49 100644 --- a/cmd/enterprise-portal/service/BUILD.bazel +++ b/cmd/enterprise-portal/service/BUILD.bazel @@ -11,7 +11,10 @@ go_library( importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/service", visibility = ["//visibility:public"], deps = [ + "//cmd/enterprise-portal/internal/codyaccessservice", "//cmd/enterprise-portal/internal/dotcomdb", + "//cmd/enterprise-portal/internal/subscriptionsservice", + "//internal/debugserver", "//internal/httpserver", "//internal/trace/policy", "//internal/version", @@ -19,7 +22,13 @@ go_library( "//lib/errors", "//lib/managedservicesplatform/cloudsql", "//lib/managedservicesplatform/runtime", + "@com_connectrpc_grpcreflect//:grpcreflect", "@com_github_jackc_pgx_v5//:pgx", "@com_github_sourcegraph_log//:log", + "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go", + "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", + "@io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp//:otelhttp", + "@org_golang_x_net//http2", + "@org_golang_x_net//http2/h2c", ], ) diff --git a/cmd/enterprise-portal/service/config.go b/cmd/enterprise-portal/service/config.go index 45e964d5a1e..e60e50608f2 100644 --- a/cmd/enterprise-portal/service/config.go +++ b/cmd/enterprise-portal/service/config.go @@ -1,6 +1,7 @@ package service import ( + sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/cloudsql" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/runtime" ) @@ -12,6 +13,14 @@ type Config struct { PGDSNOverride *string } + + SAMS SAMSConfig +} + +type SAMSConfig struct { + sams.ConnConfig + ClientID string + ClientSecret string } func (c *Config) Load(env *runtime.Env) { @@ -23,4 +32,10 @@ func (c *Config) Load(env *runtime.Env) { } c.DotComDB.PGDSNOverride = env.GetOptional("DOTCOM_PGDSN_OVERRIDE", "For local dev: custom PostgreSQL DSN, overrides DOTCOM_CLOUDSQL_* options") + + c.SAMS.ConnConfig = sams.NewConnConfigFromEnv(env) + c.SAMS.ClientID = env.Get("ENTERPRISE_PORTAL_SAMS_CLIENT_ID", "", + "Sourcegraph Accounts Management System client ID") + c.SAMS.ClientSecret = env.Get("ENTERPRISE_PORTAL_SAMS_CLIENT_SECRET", "", + "Sourcegraph Accounts Management System client secret") } diff --git a/cmd/enterprise-portal/service/service.go b/cmd/enterprise-portal/service/service.go index 3e6523ddbd8..7a071198477 100644 --- a/cmd/enterprise-portal/service/service.go +++ b/cmd/enterprise-portal/service/service.go @@ -6,8 +6,19 @@ import ( "net/http" "time" + "connectrpc.com/grpcreflect" "github.com/sourcegraph/log" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/codyaccessservice" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/subscriptionsservice" + + sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go" + "github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes" + + "github.com/sourcegraph/sourcegraph/internal/debugserver" "github.com/sourcegraph/sourcegraph/internal/httpserver" "github.com/sourcegraph/sourcegraph/internal/trace/policy" "github.com/sourcegraph/sourcegraph/internal/version" @@ -32,27 +43,94 @@ func (Service) Initialize(ctx context.Context, logger log.Logger, contract runti if err != nil { return nil, errors.Wrap(err, "newDotComDBConn") } - defer dotcomDB.Close(context.Background()) // Validate connection on startup - if err := dotcomDB.Ping(context.Background()); err != nil { + if err := dotcomDB.Ping(ctx); err != nil { return nil, errors.Wrap(err, "dotcomDB.Ping") } + logger.Debug("connected to dotcom database") + + // Prepare SAMS client, so that we can enforce SAMS-based M2M authz/authn + logger.Debug("using SAMS client", + log.String("samsExternalURL", config.SAMS.ExternalURL), + log.Stringp("samsAPIURL", config.SAMS.APIURL), + log.String("clientID", config.SAMS.ClientID)) + samsClient, err := sams.NewClientV1( + sams.ClientV1Config{ + ConnConfig: config.SAMS.ConnConfig, + TokenSource: sams.ClientCredentialsTokenSource( + config.SAMS.ConnConfig, + config.SAMS.ClientID, + config.SAMS.ClientSecret, + []scopes.Scope{scopes.OpenID, scopes.Profile, scopes.Email}, + ), + }, + ) + if err != nil { + return nil, errors.Wrap(err, "create Sourcegraph Accounts client") + } httpServer := http.NewServeMux() // Register MSP endpoints contract.Diagnostics.RegisterDiagnosticsHandlers(httpServer, serviceState{}) - // Initialize server + // Register connect endpoints + codyaccessservice.RegisterV1(logger, httpServer, samsClient.Tokens(), dotcomDB) + subscriptionsservice.RegisterV1(logger, httpServer) + listenAddr := fmt.Sprintf(":%d", contract.Port) + if !contract.MSP && debugserver.GRPCWebUIEnabled { + // Enable reflection for the web UI + reflector := grpcreflect.NewStaticReflector( + codyaccessservice.Name, + subscriptionsservice.Name, + ) + httpServer.Handle(grpcreflect.NewHandlerV1(reflector)) + httpServer.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) // web UI still requires old API + // Enable the web UI + grpcUI := debugserver.NewGRPCWebUIEndpoint("enterprise-portal", listenAddr) + httpServer.Handle(grpcUI.Path, grpcUI.Handler) + logger.Warn("gRPC web UI enabled", log.String("url", fmt.Sprintf("%s%s", listenAddr, grpcUI.Path))) + } + + // Initialize server server := httpserver.NewFromAddr( listenAddr, &http.Server{ - ReadTimeout: 2 * time.Minute, - WriteTimeout: 2 * time.Minute, - Handler: httpServer, + Addr: listenAddr, + // Cloud Run only supports HTTP/2 if the service accepts HTTP/2 cleartext (h2c), + // see https://cloud.google.com/run/docs/configuring/http2 + Handler: h2c.NewHandler( + otelhttp.NewHandler( + httpServer, + "handler", + // Don't trust incoming spans, start our own. + otelhttp.WithPublicEndpoint(), + // Generate custom span names from the request, the default is very vague. + otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string { + // Prefix with 'handle' because outgoing HTTP requests can have similar-looking + // spans. + return fmt.Sprintf("handle.%s %s", r.Method, r.URL.Path) + }), + ), + &http2.Server{}, + ), + ReadTimeout: 30 * time.Second, + WriteTimeout: time.Minute, }, ) - return server, nil + return background.LIFOStopRoutine{ + background.CallbackRoutine{ + StopFunc: func(ctx context.Context) error { + start := time.Now() + if err := dotcomDB.Close(ctx); err != nil { + return errors.Wrap(err, "dotcomDB.Close") + } + logger.Info("database stopped", log.Duration("elapsed", time.Since(start))) + return nil + }, + }, + server, // stop server first + }, nil } diff --git a/cmd/frontend/dotcomproductsubscriptiontest/dotcomproductsubscriptiontest.go b/cmd/frontend/dotcomproductsubscriptiontest/dotcomproductsubscriptiontest.go index 794d5927adf..777dabeaa90 100644 --- a/cmd/frontend/dotcomproductsubscriptiontest/dotcomproductsubscriptiontest.go +++ b/cmd/frontend/dotcomproductsubscriptiontest/dotcomproductsubscriptiontest.go @@ -35,6 +35,10 @@ func (s *subscriptionsDB) UpdateCodyGatewayAccess(ctx context.Context, id string }) } +func (s *subscriptionsDB) Archive(ctx context.Context, id string) error { + return productsubscription.NewSubscriptionsDB(s.db).Archive(ctx, id) +} + // NewSubscriptionsDB returns a new SubscriptionsDB backed by the given database.DB. // It requires testing.T to indicate that it should only be used in tests. // @@ -57,6 +61,19 @@ func NewLicensesDB(t *testing.T, db database.DB) LicensesDB { return productsubscription.NewLicensesDB(db) } +type TokensDB interface { + LookupProductSubscriptionIDByAccessToken(ctx context.Context, token string) (string, error) +} + +// NewTokensDB returns a new TokensDB backed by the given database.DB. +// It requires testing.T to indicate that it should only be used in tests. +// +// See package docs for more details. +func NewTokensDB(t *testing.T, db database.DB) TokensDB { + t.Helper() + return productsubscription.NewTokensDB(db) +} + type mockAdminFetcher struct{} func (mockAdminFetcher) GetByID(context.Context, int32) (*types.User, error) { diff --git a/cmd/frontend/internal/dotcom/productsubscription/codygateway_dotcom_user.go b/cmd/frontend/internal/dotcom/productsubscription/codygateway_dotcom_user.go index 15baccb4006..c64fc2ddfb9 100644 --- a/cmd/frontend/internal/dotcom/productsubscription/codygateway_dotcom_user.go +++ b/cmd/frontend/internal/dotcom/productsubscription/codygateway_dotcom_user.go @@ -61,7 +61,7 @@ func (r CodyGatewayDotcomUserResolver) CodyGatewayDotcomUserByToken(ctx context. return nil, err } - dbTokens := newDBTokens(r.DB) + dbTokens := NewTokensDB(r.DB) userID, err := dbTokens.LookupDotcomUserIDByAccessToken(ctx, args.Token) if err != nil { if errcode.IsNotFound(err) { diff --git a/cmd/frontend/internal/dotcom/productsubscription/tokens_db.go b/cmd/frontend/internal/dotcom/productsubscription/tokens_db.go index d9ed7ffbf11..a98581a60c3 100644 --- a/cmd/frontend/internal/dotcom/productsubscription/tokens_db.go +++ b/cmd/frontend/internal/dotcom/productsubscription/tokens_db.go @@ -22,7 +22,9 @@ type dbTokens struct { store *basestore.Store } -func newDBTokens(db database.DB) dbTokens { +// For package dotcomproductsubscriptiontest only; DO NOT USE from outside this +// package. +func NewTokensDB(db database.DB) dbTokens { return dbTokens{store: basestore.NewWithHandle(db.Handle())} } diff --git a/cmd/frontend/internal/dotcom/productsubscription/tokens_db_test.go b/cmd/frontend/internal/dotcom/productsubscription/tokens_db_test.go index 0f4649f8a64..2e8cb501bbc 100644 --- a/cmd/frontend/internal/dotcom/productsubscription/tokens_db_test.go +++ b/cmd/frontend/internal/dotcom/productsubscription/tokens_db_test.go @@ -43,7 +43,7 @@ func TestLookupProductSubscriptionIDByAccessToken(t *testing.T) { accessToken := license.GenerateLicenseKeyBasedAccessToken(lc.LicenseKey) - gotPS, err := newDBTokens(db).LookupProductSubscriptionIDByAccessToken(ctx, accessToken) + gotPS, err := NewTokensDB(db).LookupProductSubscriptionIDByAccessToken(ctx, accessToken) require.NoError(t, err) assert.Equal(t, gotPS, ps) }) @@ -55,7 +55,7 @@ func TestLookupProductSubscriptionIDByAccessToken(t *testing.T) { accessToken := license.GenerateLicenseKeyBasedAccessToken(lc.LicenseKey) accessToken = productsubscription.AccessTokenPrefix + accessToken[len(license.LicenseKeyBasedAccessTokenPrefix):] - gotPS, err := newDBTokens(db).LookupProductSubscriptionIDByAccessToken(ctx, accessToken) + gotPS, err := NewTokensDB(db).LookupProductSubscriptionIDByAccessToken(ctx, accessToken) require.NoError(t, err) assert.Equal(t, gotPS, ps) }) @@ -95,7 +95,7 @@ func TestLookupProductSubscriptionIDByAccessToken(t *testing.T) { t.Fatal("last_used_at was not nil upon token creation") } - dbTokens := newDBTokens(db) + dbTokens := NewTokensDB(db) // Call LookupDotcomUserIDByAccessToken. This will have a side-effect of updating the // token's last_used_at column. diff --git a/cmd/frontend/internal/dotcom/productsubscription/tokens_graphql.go b/cmd/frontend/internal/dotcom/productsubscription/tokens_graphql.go index 19c180de3fd..de97918596b 100644 --- a/cmd/frontend/internal/dotcom/productsubscription/tokens_graphql.go +++ b/cmd/frontend/internal/dotcom/productsubscription/tokens_graphql.go @@ -32,7 +32,7 @@ func (r ProductSubscriptionLicensingResolver) ProductSubscriptionByAccessToken(c return nil, err } - subID, err := newDBTokens(r.DB).LookupProductSubscriptionIDByAccessToken(ctx, args.AccessToken) + subID, err := NewTokensDB(r.DB).LookupProductSubscriptionIDByAccessToken(ctx, args.AccessToken) if err != nil { if errcode.IsNotFound(err) { return nil, ErrProductSubscriptionNotFound{err} diff --git a/deps.bzl b/deps.bzl index 60fc792f8e2..fbee75692bc 100644 --- a/deps.bzl +++ b/deps.bzl @@ -44,6 +44,13 @@ def go_dependencies(): sum = "h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis=", version = "v1.16.1", ) + go_repository( + name = "com_connectrpc_grpcreflect", + build_file_proto_mode = "disable_global", + importpath = "connectrpc.com/grpcreflect", + sum = "h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U=", + version = "v1.2.0", + ) go_repository( name = "com_connectrpc_otelconnect", build_file_proto_mode = "disable_global", @@ -5696,11 +5703,12 @@ def go_dependencies(): name = "com_github_sourcegraph_sourcegraph_accounts_sdk_go", build_directives = [ "gazelle:resolve go github.com/sourcegraph/sourcegraph/lib/errors @//lib/errors", + "gazelle:resolve go github.com/sourcegraph/sourcegraph/lib/background @//lib/background", ], build_file_proto_mode = "disable_global", importpath = "github.com/sourcegraph/sourcegraph-accounts-sdk-go", - sum = "h1:lOQJ+wDbQ5lSBuAv6GgCuoFKucte5k2bPf1a7navsd0=", - version = "v0.0.0-20240426173441-db5b0a145ceb", + sum = "h1:55o/Oo+gFRmE5tmFod6M/koth7RFtgRxfApjBxxtORI=", + version = "v0.0.0-20240524154739-87189364d07f", ) go_repository( name = "com_github_sourcegraph_zoekt", @@ -6322,8 +6330,8 @@ def go_dependencies(): name = "com_google_cloud_go", build_file_proto_mode = "disable_global", importpath = "cloud.google.com/go", - sum = "h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM=", - version = "v0.112.0", + sum = "h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=", + version = "v0.112.1", ) go_repository( name = "com_google_cloud_go_accessapproval", @@ -6924,8 +6932,8 @@ def go_dependencies(): name = "com_google_cloud_go_pubsub", build_file_proto_mode = "disable_global", importpath = "cloud.google.com/go/pubsub", - sum = "h1:dfEPuGCHGbWUhaMCTHUFjfroILEkx55iUmKBZTP5f+Y=", - version = "v1.36.1", + sum = "h1:0uEEfaB1VIJzabPpwpZf44zWAKAme3zwKKxHk7vJQxQ=", + version = "v1.37.0", ) go_repository( name = "com_google_cloud_go_pubsublite", @@ -7050,8 +7058,8 @@ def go_dependencies(): name = "com_google_cloud_go_storage", build_file_proto_mode = "disable_global", importpath = "cloud.google.com/go/storage", - sum = "h1:WI8CsaFO8Q9KjPVtsZ5Cmi0dXV25zMoX0FklT7c3Jm4=", - version = "v1.37.0", + sum = "h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg=", + version = "v1.38.0", ) go_repository( name = "com_google_cloud_go_storagetransfer", @@ -8059,8 +8067,8 @@ def go_dependencies(): name = "org_golang_google_protobuf", build_file_proto_mode = "disable_global", importpath = "google.golang.org/protobuf", - sum = "h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=", - version = "v1.33.0", + sum = "h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=", + version = "v1.34.0", ) go_repository( name = "org_golang_x_crypto", diff --git a/go.mod b/go.mod index c3feadbb9c6..22ab0b93336 100644 --- a/go.mod +++ b/go.mod @@ -60,9 +60,9 @@ require ( cloud.google.com/go/kms v1.15.7 cloud.google.com/go/monitoring v1.18.0 cloud.google.com/go/profiler v0.4.0 - cloud.google.com/go/pubsub v1.36.1 + cloud.google.com/go/pubsub v1.37.0 cloud.google.com/go/secretmanager v1.11.5 - cloud.google.com/go/storage v1.37.0 + cloud.google.com/go/storage v1.38.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.45.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.21.0 github.com/Khan/genqlient v0.5.0 @@ -230,7 +230,7 @@ require ( gonum.org/v1/gonum v0.14.0 google.golang.org/api v0.169.0 google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/protobuf v1.33.0 + google.golang.org/protobuf v1.34.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -248,6 +248,7 @@ require ( chainguard.dev/apko v0.14.0 cloud.google.com/go/artifactregistry v1.14.8 connectrpc.com/connect v1.16.1 + connectrpc.com/grpcreflect v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.5.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 @@ -299,8 +300,8 @@ require ( github.com/sourcegraph/managed-services-platform-cdktf/gen/tfe v0.0.0-20240513203650-e2b1273f1c1a github.com/sourcegraph/notionreposync v0.0.0-20240510213306-87052870048d github.com/sourcegraph/scip v0.3.3 - github.com/sourcegraph/sourcegraph-accounts-sdk-go v0.0.0-20240426173441-db5b0a145ceb - github.com/sourcegraph/sourcegraph/lib v0.0.0-20240422195121-52350cd2e507 + github.com/sourcegraph/sourcegraph-accounts-sdk-go v0.0.0-20240524154739-87189364d07f + github.com/sourcegraph/sourcegraph/lib v0.0.0-20240524140455-2589fef13ea8 github.com/sourcegraph/sourcegraph/lib/managedservicesplatform v0.0.0-00010101000000-000000000000 github.com/sourcegraph/sourcegraph/monitoring v0.0.0-00010101000000-000000000000 github.com/vektah/gqlparser/v2 v2.4.5 @@ -438,7 +439,7 @@ require ( require ( bitbucket.org/creachadair/shell v0.0.7 // indirect - cloud.google.com/go v0.112.0 // indirect + cloud.google.com/go v0.112.1 // indirect cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/iam v1.1.6 // indirect cuelang.org/go v0.4.3 diff --git a/go.sum b/go.sum index 386ff659543..18a2371d203 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= -cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= cloud.google.com/go/artifactregistry v1.14.8 h1:icIyRzJ1Ag6EOafuDuFFJ/AdStcOFRVfSGURn27/7Pk= cloud.google.com/go/artifactregistry v1.14.8/go.mod h1:1UlSXh6sTXYrIT4kMO21AE1IDlMFemlZuX6QS+JXW7I= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -64,8 +64,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.36.1 h1:dfEPuGCHGbWUhaMCTHUFjfroILEkx55iUmKBZTP5f+Y= -cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE= +cloud.google.com/go/pubsub v1.37.0 h1:0uEEfaB1VIJzabPpwpZf44zWAKAme3zwKKxHk7vJQxQ= +cloud.google.com/go/pubsub v1.37.0/go.mod h1:YQOQr1uiUM092EXwKs56OPT650nwnawc+8/IjoUeGzQ= cloud.google.com/go/secretmanager v1.11.5 h1:82fpF5vBBvu9XW4qj0FU2C6qVMtj1RM/XHwKXUEAfYY= cloud.google.com/go/secretmanager v1.11.5/go.mod h1:eAGv+DaCHkeVyQi0BeXgAHOU0RdrMeZIASKc+S7VqH4= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= @@ -73,12 +73,14 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.37.0 h1:WI8CsaFO8Q9KjPVtsZ5Cmi0dXV25zMoX0FklT7c3Jm4= -cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k= +cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= +cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= cloud.google.com/go/trace v1.10.5 h1:0pr4lIKJ5XZFYD9GtxXEWr0KkVeigc3wlGpZco0X1oA= cloud.google.com/go/trace v1.10.5/go.mod h1:9hjCV1nGBCtXbAE4YK7OqJ8pmPYSxPA0I67JwRd5s3M= connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis= connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY= connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc= contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= @@ -1747,8 +1749,8 @@ github.com/sourcegraph/run v0.12.0 h1:3A8w5e8HIYPfafHekvmdmmh42RHKGVhmiTZAPJclg7 github.com/sourcegraph/run v0.12.0/go.mod h1:PwaP936BTnAJC1cqR5rSbG5kOs/EWStTK3lqvMX5GUA= github.com/sourcegraph/scip v0.3.3 h1:3EOkChYOntwHl0pPSAju7rj0oRuujh8owC4vjGDEr0s= github.com/sourcegraph/scip v0.3.3/go.mod h1:Q67VaoTpftINIy/CLrkYQOMwlsx67h8ys+ligmdUcqM= -github.com/sourcegraph/sourcegraph-accounts-sdk-go v0.0.0-20240426173441-db5b0a145ceb h1:lOQJ+wDbQ5lSBuAv6GgCuoFKucte5k2bPf1a7navsd0= -github.com/sourcegraph/sourcegraph-accounts-sdk-go v0.0.0-20240426173441-db5b0a145ceb/go.mod h1:xul4Fiph3Pvdx/1qsmhCUL2GBeYjTcnga0LXZEbKdGo= +github.com/sourcegraph/sourcegraph-accounts-sdk-go v0.0.0-20240524154739-87189364d07f h1:55o/Oo+gFRmE5tmFod6M/koth7RFtgRxfApjBxxtORI= +github.com/sourcegraph/sourcegraph-accounts-sdk-go v0.0.0-20240524154739-87189364d07f/go.mod h1:+yFgPzr01Ks+pcvFlStxKtUp4Wq//BqQEJDnZczD3h0= github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152 h1:z/MpntplPaW6QW95pzcAR/72Z5TWDyDnSo0EOcyij9o= github.com/sourcegraph/yaml v1.0.1-0.20200714132230-56936252f152/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/sourcegraph/zoekt v0.0.0-20240514170004-fe8f2a3d9cab h1:9g0lbxps+Yr99nE2Y8RnhPwV6HUYUxEpHfB+RISFfdU= @@ -2615,8 +2617,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= diff --git a/internal/debugserver/grpcui.go b/internal/debugserver/grpcui.go index 22167b4dbb4..0116c5f8f71 100644 --- a/internal/debugserver/grpcui.go +++ b/internal/debugserver/grpcui.go @@ -6,14 +6,17 @@ import ( "github.com/fullstorydev/grpcui/standalone" "github.com/sourcegraph/log" - "github.com/sourcegraph/sourcegraph/internal/env" "google.golang.org/grpc" + "github.com/sourcegraph/sourcegraph/internal/env" + "github.com/sourcegraph/sourcegraph/internal/grpc/defaults" "github.com/sourcegraph/sourcegraph/lib/errors" ) -var envEnableGRPCWebUI = env.MustGetBool("GRPC_WEB_UI_ENABLED", false, "Enable the gRPC Web UI to debug and explore gRPC services") +// GRPCWebUIEnabled is an additional environment variable that must be true to +// enable the gRPC Web UI. +var GRPCWebUIEnabled = env.MustGetBool("GRPC_WEB_UI_ENABLED", false, "Enable the gRPC Web UI to debug and explore gRPC services") const gRPCWebUIPath = "/debug/grpcui" @@ -53,7 +56,7 @@ type grpcHandler struct { } func (g *grpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !envEnableGRPCWebUI { + if !GRPCWebUIEnabled { http.Error(w, "gRPC Web UI is disabled", http.StatusNotFound) return } diff --git a/lib/enterpriseportal/codyaccess/v1/codyaccess.proto b/lib/enterpriseportal/codyaccess/v1/codyaccess.proto index 21be30bebc6..ee4cc507f29 100644 --- a/lib/enterpriseportal/codyaccess/v1/codyaccess.proto +++ b/lib/enterpriseportal/codyaccess/v1/codyaccess.proto @@ -64,7 +64,8 @@ message CodyGatewayAccessToken { message CodyGatewayAccess { // The external, prefixed UUID-format identifier for the Enterprise - // subscription corresponding to this Cody Gateway access description. + // subscription corresponding to this Cody Gateway access description + // (e.g. "es_..."). string subscription_id = 1; // Whether or not a subscription has Cody Gateway access enabled. diff --git a/lib/enterpriseportal/subscriptions/v1/BUILD.bazel b/lib/enterpriseportal/subscriptions/v1/BUILD.bazel index 4a64c1ff48a..1f42bb9e7d4 100644 --- a/lib/enterpriseportal/subscriptions/v1/BUILD.bazel +++ b/lib/enterpriseportal/subscriptions/v1/BUILD.bazel @@ -22,6 +22,7 @@ go_library( srcs = [ "subscriptions.pb.go", "subscriptions_grpc.pb.go", + "v1.go", ], importpath = "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1", visibility = ["//visibility:public"], diff --git a/lib/enterpriseportal/subscriptions/v1/subscriptions.proto b/lib/enterpriseportal/subscriptions/v1/subscriptions.proto index f855d634388..8860b04287c 100644 --- a/lib/enterpriseportal/subscriptions/v1/subscriptions.proto +++ b/lib/enterpriseportal/subscriptions/v1/subscriptions.proto @@ -56,7 +56,8 @@ message EnterpriseSubscriptionCondition { // EnterpriseSubscription represents a Sourcegraph Enterprise subscription. message EnterpriseSubscription { - // ID is the external, prefixed UUID-format identifier for this subscription. + // ID is the external, prefixed UUID-format identifier for this subscription + // (e.g. "es_..."). string id = 1; // Timeline of key events corresponding to this subscription. repeated EnterpriseSubscriptionCondition conditions = 2; diff --git a/lib/enterpriseportal/subscriptions/v1/v1.go b/lib/enterpriseportal/subscriptions/v1/v1.go new file mode 100644 index 00000000000..9ab810bc862 --- /dev/null +++ b/lib/enterpriseportal/subscriptions/v1/v1.go @@ -0,0 +1,7 @@ +package v1 + +const ( + // EnterpriseSubscriptionIDPrefix is the prefix for a subscription ID + // ('es' for 'Enterprise Subscription'). + EnterpriseSubscriptionIDPrefix = "es_" +) diff --git a/sg.config.yaml b/sg.config.yaml index 5925fab212f..e723aba027a 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -386,6 +386,8 @@ commands: enterprise-portal: cmd: | + # Connect to local development database, with the assumption that it will + # have dotcom database tables. export DOTCOM_PGDSN_OVERRIDE="postgres://$PGUSER:$PGPASSWORD@$PGHOST:$PGPORT/$PGDATABASE?sslmode=$PGSSLMODE" .bin/enterprise-portal install: | @@ -398,6 +400,12 @@ commands: PORT: '6081' DIAGNOSTICS_SECRET: sekret SRC_LOG_LEVEL: debug + GRPC_WEB_UI_ENABLED: 'true' + # Used for authentication + SAMS_URL: https://accounts.sgdev.org + # Set real values in sg.config.yaml overrides + ENTERPRISE_PORTAL_SAMS_CLIENT_ID: "sams_cid_put_a_real_value_in_sg.config.overwrite.yaml" + ENTERPRISE_PORTAL_SAMS_CLIENT_SECRET: "sams_cs_put_a_real_value_in_sg.config.overwrite.yaml" watch: - lib - cmd/enterprise-portal