Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit b717fd5

Browse files
unknwonbobheadxi
andauthored
enterprise-portal: implement basic MSP IAM and RPCs (#63173)
Closes CORE-99, closes CORE-176 This PR is based off (and was also served as PoC of) [RFC 962: MSP IAM framework](https://docs.google.com/document/d/1ItJlQnpR5AHbrfAholZqjH8-8dPF1iQcKh99gE6SSjs/edit). It comes with two main parts: 1. The initial version of the MSP IAM SDK: `lib/managedservicesplatform/iam` - Embeds the [OpenFGA server implementation](https://github.com/openfga/openfga/tree/main/pkg/server) and exposes the a `ClientV1` for interacting with it. - Automagically manages the both MSP IAM's and OpenFGA's database migrations upon initializing the `ClientV1`. ![CleanShot 2024-06-18 at 15 09 24@2x](https://github.com/sourcegraph/sourcegraph/assets/2946214/387e0e28-a6c2-4664-b946-0ea4a1dd0804) - Ensures the specified OpenFGA's store and automatization model DSL exists. - Utility types and helpers to avoid easy mistakes (i.e. make the relation tuples a bit more strongly-typed). - Decided to put all types and pre-defined values together to simulate a "central registry" and acting as a forcing function for services to form some sort of convention. Then when we migrate the OpenFGA server to a separate standalone service, it will be less headache about consolidating similar meaning types/relations but different string literals. 1. The first use case of the MSP IAM: `cmd/enterprise-portal/internal/subscriptionsservice` - Added/updated RPCs: - Listing enterprise subscriptions via permissions - Update enterprise subscriptions to assign instance domains - Update enterprise subscriptions membership to assign roles (and permissions) - A database table for enterprise subscriptions, only storing the extra instance domains as Enterprise Portal is not the writeable-source-of-truth. ## Other minor changes - Moved `internal/redislock` to `lib/redislock` to be used in MSP IAM SDK. - Call `createdb ...` as part of `enterprise-portal` install script in `sg.config.yaml` (`msp_iam` database is a hard requirement of MSP IAM framework). ## Test plan Tested with gRPC UI: - `UpdateEnterpriseSubscription` to assign an instance domain - `UpdateEnterpriseSubscriptionMembership` to assign roles - `ListEnterpriseSubscriptions`: - List by subscription ID - List by instance domain - List by view cody analytics permissions --------- Co-authored-by: Robert Lin <robert@bobheadxi.dev>
1 parent 5630eef commit b717fd5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+5433
-1022
lines changed

cmd/enterprise-portal/internal/codyaccessservice/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ go_library(
66
srcs = [
77
"adapters.go",
88
"v1.go",
9+
"v1_store.go",
910
],
1011
importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/codyaccessservice",
1112
tags = [TAG_INFRA_CORESERVICES],
@@ -21,6 +22,7 @@ go_library(
2122
"//lib/errors",
2223
"@com_connectrpc_connect//:connect",
2324
"@com_github_sourcegraph_log//:log",
25+
"@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go",
2426
"@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes",
2527
"@org_golang_google_protobuf//types/known/durationpb",
2628
],

cmd/enterprise-portal/internal/codyaccessservice/v1.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ type DotComDB interface {
3030
func RegisterV1(
3131
logger log.Logger,
3232
mux *http.ServeMux,
33-
samsClient samsm2m.TokenIntrospector,
33+
store StoreV1,
3434
dotcom DotComDB,
3535
opts ...connect.HandlerOption,
3636
) {
3737
mux.Handle(
3838
codyaccessv1connect.NewCodyAccessServiceHandler(
3939
&handlerV1{
40-
logger: logger.Scoped("codyaccess.v1"),
41-
samsClient: samsClient,
42-
dotcom: dotcom,
40+
logger: logger.Scoped("codyaccess.v1"),
41+
store: store,
42+
dotcom: dotcom,
4343
},
4444
opts...,
4545
),
@@ -48,10 +48,10 @@ func RegisterV1(
4848

4949
type handlerV1 struct {
5050
codyaccessv1connect.UnimplementedCodyAccessServiceHandler
51-
logger log.Logger
5251

53-
samsClient samsm2m.TokenIntrospector
54-
dotcom DotComDB
52+
logger log.Logger
53+
store StoreV1
54+
dotcom DotComDB
5555
}
5656

5757
var _ codyaccessv1connect.CodyAccessServiceHandler = (*handlerV1)(nil)
@@ -62,7 +62,7 @@ func (s *handlerV1) GetCodyGatewayAccess(ctx context.Context, req *connect.Reque
6262

6363
// 🚨 SECURITY: Require approrpiate M2M scope.
6464
requiredScope := samsm2m.EnterprisePortalScope("codyaccess", scopes.ActionRead)
65-
clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.samsClient, requiredScope, req)
65+
clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req)
6666
if err != nil {
6767
return nil, err
6868
}
@@ -106,7 +106,7 @@ func (s *handlerV1) ListCodyGatewayAccesses(ctx context.Context, req *connect.Re
106106

107107
// 🚨 SECURITY: Require approrpiate M2M scope.
108108
requiredScope := samsm2m.EnterprisePortalScope("codyaccess", scopes.ActionRead)
109-
clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.samsClient, requiredScope, req)
109+
clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req)
110110
if err != nil {
111111
return nil, err
112112
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package codyaccessservice
2+
3+
import (
4+
"context"
5+
6+
sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go"
7+
)
8+
9+
// StoreV1 is the data layer carrier for Cody access service v1. This interface
10+
// is meant to abstract away and limit the exposure of the underlying data layer
11+
// to the handler through a thin-wrapper.
12+
type StoreV1 interface {
13+
// IntrospectSAMSToken takes a SAMS access token and returns relevant metadata.
14+
//
15+
// 🚨SECURITY: SAMS will return a successful result if the token is valid, but
16+
// is no longer active. It is critical that the caller not honor tokens where
17+
// `.Active == false`.
18+
IntrospectSAMSToken(ctx context.Context, token string) (*sams.IntrospectTokenResponse, error)
19+
}
20+
21+
type storeV1 struct {
22+
SAMSClient *sams.ClientV1
23+
}
24+
25+
type StoreV1Options struct {
26+
SAMSClient *sams.ClientV1
27+
}
28+
29+
// NewStoreV1 returns a new StoreV1 using the given resource handles.
30+
func NewStoreV1(opts StoreV1Options) StoreV1 {
31+
return &storeV1{
32+
SAMSClient: opts.SAMSClient,
33+
}
34+
}
35+
36+
func (s *storeV1) IntrospectSAMSToken(ctx context.Context, token string) (*sams.IntrospectTokenResponse, error) {
37+
return s.SAMSClient.Tokens().IntrospectToken(ctx, token)
38+
}
Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,55 @@
1+
load("//dev:go_defs.bzl", "go_test")
12
load("@io_bazel_rules_go//go:def.bzl", "go_library")
23

34
go_library(
45
name = "database",
56
srcs = [
67
"database.go",
78
"migrate.go",
8-
"permissions.go",
99
"subscriptions.go",
1010
],
1111
importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database",
1212
visibility = ["//cmd/enterprise-portal:__subpackages__"],
1313
deps = [
14-
"//internal/redislock",
1514
"//lib/errors",
1615
"//lib/managedservicesplatform/runtime",
16+
"//lib/redislock",
17+
"@com_github_jackc_pgx_v5//:pgx",
1718
"@com_github_jackc_pgx_v5//pgxpool",
1819
"@com_github_redis_go_redis_v9//:go-redis",
1920
"@com_github_sourcegraph_log//:log",
2021
"@io_gorm_driver_postgres//:postgres",
2122
"@io_gorm_gorm//:gorm",
2223
"@io_gorm_gorm//logger",
24+
"@io_gorm_gorm//schema",
2325
"@io_gorm_plugin_opentelemetry//tracing",
2426
"@io_opentelemetry_go_otel//:otel",
2527
"@io_opentelemetry_go_otel//attribute",
2628
"@io_opentelemetry_go_otel//codes",
2729
"@io_opentelemetry_go_otel_trace//:trace",
2830
],
2931
)
32+
33+
go_test(
34+
name = "database_test",
35+
srcs = [
36+
"main_test.go",
37+
"subscriptions_test.go",
38+
],
39+
embed = [":database"],
40+
tags = [
41+
# Test requires localhost database
42+
"requires-network",
43+
],
44+
deps = [
45+
"//internal/database/dbtest",
46+
"@com_github_google_uuid//:uuid",
47+
"@com_github_jackc_pgx_v5//:pgx",
48+
"@com_github_jackc_pgx_v5//pgxpool",
49+
"@com_github_stretchr_testify//assert",
50+
"@com_github_stretchr_testify//require",
51+
"@io_gorm_driver_postgres//:postgres",
52+
"@io_gorm_gorm//:gorm",
53+
"@io_gorm_gorm//schema",
54+
],
55+
)

cmd/enterprise-portal/internal/database/database.go

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ package database
22

33
import (
44
"context"
5+
"os"
56

7+
"github.com/jackc/pgx/v5"
68
"github.com/jackc/pgx/v5/pgxpool"
79
"github.com/redis/go-redis/v9"
810
"github.com/sourcegraph/log"
911
"go.opentelemetry.io/otel"
12+
"gorm.io/gorm/schema"
1013

1114
"github.com/sourcegraph/sourcegraph/lib/errors"
1215
"github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/runtime"
@@ -19,13 +22,23 @@ type DB struct {
1922
db *pgxpool.Pool
2023
}
2124

25+
func (db *DB) Subscriptions() *SubscriptionsStore {
26+
return newSubscriptionsStore(db.db)
27+
}
28+
2229
// ⚠️ WARNING: This list is meant to be read-only.
23-
var allTables = []any{
30+
var allTables = []schema.Tabler{
2431
&Subscription{},
25-
&Permission{},
2632
}
2733

28-
const databaseName = "enterprise-portal"
34+
func databaseName(msp bool) string {
35+
if msp {
36+
return "enterprise_portal"
37+
}
38+
39+
// Use whatever the current database is for local development.
40+
return os.Getenv("PGDATABASE")
41+
}
2942

3043
// NewHandle returns a new database handle with the given configuration. It may
3144
// attempt to auto-migrate the database schema if the application version has
@@ -36,9 +49,34 @@ func NewHandle(ctx context.Context, logger log.Logger, contract runtime.Contract
3649
return nil, errors.Wrap(err, "maybe migrate")
3750
}
3851

39-
pool, err := contract.PostgreSQL.GetConnectionPool(ctx, databaseName)
52+
pool, err := contract.PostgreSQL.GetConnectionPool(ctx, databaseName(contract.MSP))
4053
if err != nil {
4154
return nil, errors.Wrap(err, "get connection pool")
4255
}
4356
return &DB{db: pool}, nil
4457
}
58+
59+
// transaction executes the given function within a transaction. If the function
60+
// returns an error, the transaction will be rolled back.
61+
func transaction(ctx context.Context, db *pgxpool.Pool, fn func(tx pgx.Tx) error) (err error) {
62+
tx, err := db.Begin(ctx)
63+
if err != nil {
64+
return errors.Wrap(err, "begin")
65+
}
66+
defer func() {
67+
rollbackErr := tx.Rollback(ctx)
68+
// Only return the rollback error if there is no other error.
69+
if err == nil {
70+
err = errors.Wrap(rollbackErr, "rollback")
71+
}
72+
}()
73+
74+
if err = fn(tx); err != nil {
75+
return err
76+
}
77+
78+
if err = tx.Commit(ctx); err != nil {
79+
return errors.Wrap(err, "commit")
80+
}
81+
return nil
82+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package database
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"strings"
8+
"testing"
9+
"time"
10+
11+
"github.com/jackc/pgx/v5/pgxpool"
12+
"github.com/stretchr/testify/require"
13+
"gorm.io/driver/postgres"
14+
"gorm.io/gorm"
15+
"gorm.io/gorm/schema"
16+
17+
"github.com/sourcegraph/sourcegraph/internal/database/dbtest"
18+
)
19+
20+
// newTestDB creates a new test database and initializes the given list of
21+
// tables for the suite. The test database is dropped after testing is completed
22+
// unless failed.
23+
//
24+
// Future: Move to a shared package when more than Enterprise Portal uses it.
25+
func newTestDB(t testing.TB, system, suite string, tables ...schema.Tabler) *pgxpool.Pool {
26+
if testing.Short() {
27+
t.Skip("skipping DB test since -short specified")
28+
}
29+
30+
dsn, err := dbtest.GetDSN()
31+
require.NoError(t, err)
32+
33+
// Open a connection to control the test database lifecycle.
34+
sqlDB, err := sql.Open("pgx", dsn.String())
35+
require.NoError(t, err)
36+
37+
// Set up test suite database.
38+
dbName := fmt.Sprintf("sourcegraph-test-%s-%s-%d", system, suite, time.Now().Unix())
39+
_, err = sqlDB.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %q", dbName))
40+
require.NoError(t, err)
41+
42+
_, err = sqlDB.Exec(fmt.Sprintf("CREATE DATABASE %q", dbName))
43+
require.NoError(t, err)
44+
45+
// Swap out the database name to be the test suite database in the DSN.
46+
dsn.Path = "/" + dbName
47+
48+
now := time.Now().UTC().Truncate(time.Second)
49+
db, err := gorm.Open(
50+
postgres.Open(dsn.String()),
51+
&gorm.Config{
52+
SkipDefaultTransaction: true,
53+
NowFunc: func() time.Time {
54+
return now
55+
},
56+
},
57+
)
58+
require.NoError(t, err)
59+
for _, table := range tables {
60+
err = db.AutoMigrate(table)
61+
require.NoError(t, err)
62+
}
63+
64+
// Close the connection used to auto-migrate the database.
65+
migrateDB, err := db.DB()
66+
require.NoError(t, err)
67+
err = migrateDB.Close()
68+
require.NoError(t, err)
69+
70+
// Open a new connection to the test suite database.
71+
testDB, err := pgxpool.New(context.Background(), dsn.String())
72+
require.NoError(t, err)
73+
74+
t.Cleanup(func() {
75+
if t.Failed() {
76+
t.Logf("Database %q left intact for inspection", dbName)
77+
return
78+
}
79+
80+
testDB.Close()
81+
82+
_, err = sqlDB.Exec(fmt.Sprintf(`DROP DATABASE %q`, dbName))
83+
if err != nil {
84+
t.Errorf("Failed to drop test suite database %q: %v", dbName, err)
85+
}
86+
err = sqlDB.Close()
87+
if err != nil {
88+
t.Errorf("Failed to close test database connection %q: %v", dbName, err)
89+
}
90+
})
91+
92+
return testDB
93+
}
94+
95+
// clearTables removes all rows from the list of tables in the original order.
96+
// It uses soft-deletion when available and skips deletion when the test suite
97+
// failed.
98+
//
99+
// Future: Move to a shared package when more than Enterprise Portal uses it.
100+
func clearTables(t *testing.T, db *pgxpool.Pool, tables ...schema.Tabler) error {
101+
if t.Failed() {
102+
return nil
103+
}
104+
105+
tableNames := make([]string, 0, len(tables))
106+
for _, t := range tables {
107+
tableNames = append(tableNames, t.TableName())
108+
}
109+
_, err := db.Exec(context.Background(), "TRUNCATE TABLE "+strings.Join(tableNames, ", ")+" RESTART IDENTITY")
110+
return err
111+
}

0 commit comments

Comments
 (0)