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 <jh@chabran.fr>
Co-authored-by: Joe Chen <joe@sourcegraph.com>
This commit is contained in:
Robert Lin 2024-05-27 13:39:57 -07:00 committed by GitHub
parent 72fcf13241
commit 704b36a143
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 765 additions and 47 deletions

View File

@ -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",
],
)

View File

@ -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
}(),
}
}

View File

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

View File

@ -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",
],
)

View File

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

View File

@ -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",

View File

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

View File

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

View File

@ -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",
],
)

View File

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

View File

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

View File

@ -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",
],
)

View File

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

View File

@ -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",
],
)

View File

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

View File

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

View File

@ -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) {

View File

@ -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) {

View File

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

View File

@ -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.

View File

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

View File

@ -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",

13
go.mod
View File

@ -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

22
go.sum
View File

@ -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=

View File

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

View File

@ -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.

View File

@ -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"],

View File

@ -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;

View File

@ -0,0 +1,7 @@
package v1
const (
// EnterpriseSubscriptionIDPrefix is the prefix for a subscription ID
// ('es' for 'Enterprise Subscription').
EnterpriseSubscriptionIDPrefix = "es_"
)

View File

@ -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