From e2c646ad9201d87642404f405db6a1b44bed98ab Mon Sep 17 00:00:00 2001 From: Robert Lin Date: Fri, 9 Aug 2024 17:26:18 -0700 Subject: [PATCH] feat/enterpriseportal: all subscriptions APIs use enterprise portal DB (#63959) This change follows https://github.com/sourcegraph/sourcegraph/pull/63858 by making the _all_ subscriptions APIs read and write to the Enterprise Portal database, instead of dotcomdb, using the data that we sync from dotcomdb into Enterprise Portal. With this PR, all initially proposed subscriptions APIs are at least partially implemented. Uses https://github.com/hexops/valast/pull/27 for custom `autogold` rendering of `utctime.Time` Closes https://linear.app/sourcegraph/issue/CORE-156 Part of https://linear.app/sourcegraph/issue/CORE-158 ## Test plan - [x] Unit tests on API level - [x] Adapters unit testing - [x] Simple E2E test: https://github.com/sourcegraph/sourcegraph/pull/64057 --- .../internal/codyaccessservice/v1_store.go | 2 + .../internal/database/importer/importer.go | 2 +- .../subscriptions/license_conditions.go | 2 +- .../database/subscriptions/licenses.go | 5 +- .../database/subscriptions/subscriptions.go | 16 +- .../subscriptions/subscriptions_conditions.go | 2 +- .../subscriptions/subscriptions_test.go | 18 +- .../internal/database/types.go | 11 + .../internal/database/utctime/BUILD.bazel | 6 +- .../internal/database/utctime/utctime.go | 5 + .../internal/database/utctime/valast.go | 31 + .../internal/subscriptionsservice/BUILD.bazel | 14 + .../internal/subscriptionsservice/adapters.go | 78 +- .../subscriptionsservice/adapters_test.go | 340 +++++++ .../subscriptionsservice/mocks_test.go | 920 +++++++++++++++++- .../internal/subscriptionsservice/v1.go | 391 +++++++- .../internal/subscriptionsservice/v1_store.go | 118 ++- .../internal/subscriptionsservice/v1_test.go | 722 +++++++++++++- cmd/enterprise-portal/service/BUILD.bazel | 2 + cmd/enterprise-portal/service/config.go | 42 + cmd/enterprise-portal/service/service.go | 8 +- deps.bzl | 5 +- go.mod | 2 + go.sum | 5 +- internal/license/generate-license.go | 2 +- internal/license/license.go | 7 + internal/licensing/licensing.go | 11 +- .../subscriptions/v1/subscriptions.pb.go | 458 +++++---- .../subscriptions/v1/subscriptions.proto | 11 + 29 files changed, 2920 insertions(+), 316 deletions(-) create mode 100644 cmd/enterprise-portal/internal/database/utctime/valast.go diff --git a/cmd/enterprise-portal/internal/codyaccessservice/v1_store.go b/cmd/enterprise-portal/internal/codyaccessservice/v1_store.go index 5b14031be3990..908c0c4d151da 100644 --- a/cmd/enterprise-portal/internal/codyaccessservice/v1_store.go +++ b/cmd/enterprise-portal/internal/codyaccessservice/v1_store.go @@ -33,6 +33,8 @@ type StoreV1 interface { // GetCodyGatewayUsage retrieves recent Cody Gateway usage data. // The subscriptionID should not be prefixed. + // + // Returns errStoreUnimplemented if the data source not configured. GetCodyGatewayUsage(ctx context.Context, subscriptionID string) (*codyaccessv1.CodyGatewayUsage, error) // GetCodyGatewayAccessBySubscription retrieves Cody Gateway access by diff --git a/cmd/enterprise-portal/internal/database/importer/importer.go b/cmd/enterprise-portal/internal/database/importer/importer.go index 3496d6feecd7c..41c2eb731d925 100644 --- a/cmd/enterprise-portal/internal/database/importer/importer.go +++ b/cmd/enterprise-portal/internal/database/importer/importer.go @@ -245,7 +245,7 @@ func (i *Importer) importSubscription(ctx context.Context, dotcomSub *dotcomdb.S } return pointers.Ptr(utctime.FromTime(*dotcomSub.ArchivedAt)) }(), - SalesforceSubscriptionID: activeLicense.SalesforceSubscriptionID, + SalesforceSubscriptionID: database.NewNullStringPtr(activeLicense.SalesforceSubscriptionID), }, conditions..., ); err != nil { diff --git a/cmd/enterprise-portal/internal/database/subscriptions/license_conditions.go b/cmd/enterprise-portal/internal/database/subscriptions/license_conditions.go index e7fb162c9377c..16582b410c831 100644 --- a/cmd/enterprise-portal/internal/database/subscriptions/license_conditions.go +++ b/cmd/enterprise-portal/internal/database/subscriptions/license_conditions.go @@ -96,7 +96,7 @@ VALUES ( )`, pgx.NamedArgs{ "licenseID": licenseID, // Convert to string representation of EnterpriseSubscriptionLicenseCondition - "status": subscriptionsv1.EnterpriseSubscriptionLicenseCondition_Status_name[int32(opts.Status)], + "status": opts.Status.String(), "message": pointers.NilIfZero(opts.Message), "transitionTime": opts.TransitionTime, }) diff --git a/cmd/enterprise-portal/internal/database/subscriptions/licenses.go b/cmd/enterprise-portal/internal/database/subscriptions/licenses.go index b8b210bdd0031..106e761269e53 100644 --- a/cmd/enterprise-portal/internal/database/subscriptions/licenses.go +++ b/cmd/enterprise-portal/internal/database/subscriptions/licenses.go @@ -362,7 +362,7 @@ VALUES ( `, pgx.NamedArgs{ "licenseID": licenseID, "subscriptionID": subscriptionID, - "licenseType": subscriptionsv1.EnterpriseSubscriptionLicenseType_name[int32(licenseType)], + "licenseType": licenseType.String(), "licenseData": licenseData, "createdAt": opts.Time, "expireAt": opts.ExpireTime, @@ -422,6 +422,9 @@ WHERE id = @licenseID "revokedAt": opts.Time, "licenseID": licenseID, }); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrSubscriptionLicenseNotFound + } return nil, errors.Wrap(err, "revoke license") } diff --git a/cmd/enterprise-portal/internal/database/subscriptions/subscriptions.go b/cmd/enterprise-portal/internal/database/subscriptions/subscriptions.go index 7a51a224c5faa..63ab65a8a618c 100644 --- a/cmd/enterprise-portal/internal/database/subscriptions/subscriptions.go +++ b/cmd/enterprise-portal/internal/database/subscriptions/subscriptions.go @@ -151,7 +151,7 @@ func (opts ListEnterpriseSubscriptionsOptions) toQueryConditions() (where, limit if *opts.IsArchived { whereConds = append(whereConds, "archived_at IS NOT NULL") } else { - whereConds = append(whereConds, "archived IS NUlL") + whereConds = append(whereConds, "archived_at IS NUlL") } } if len(opts.DisplayNameSubstring) > 0 { @@ -221,7 +221,7 @@ type UpsertSubscriptionOptions struct { CreatedAt utctime.Time ArchivedAt *utctime.Time - SalesforceSubscriptionID *string + SalesforceSubscriptionID *sql.NullString // ForceUpdate indicates whether to force update all fields of the subscription // record. @@ -249,9 +249,14 @@ func (opts UpsertSubscriptionOptions) apply(ctx context.Context, db upsert.Exece return b.Exec(ctx, db) } +var ErrInvalidArgument = errors.New("invalid argument") + // Upsert upserts a subscription record based on the given options. If the // operation has additional application meaning, conditions can be provided // for insert as well. +// +// Constraint errors are returned as a human-friendly error that wraps +// ErrInvalidArgument. func (s *Store) Upsert( ctx context.Context, subscriptionID string, @@ -281,7 +286,12 @@ func (s *Store) Upsert( if err := opts.apply(ctx, tx, subscriptionID); err != nil { if pgxerrors.IsContraintError(err, "idx_enterprise_portal_subscriptions_display_name") { return nil, errors.WithSafeDetails( - errors.Newf("display_name %q is already in use", opts.DisplayName.String), + errors.Wrapf(ErrInvalidArgument, "display_name %q is already in use", opts.DisplayName.String), + "%+v", err) + } + if pgxerrors.IsContraintError(err, "idx_enterprise_portal_subscriptions_instance_domain") { + return nil, errors.WithSafeDetails( + errors.Wrapf(ErrInvalidArgument, "instance_domain %q is assigned to another subscription", opts.DisplayName.String), "%+v", err) } return nil, errors.Wrap(err, "upsert") diff --git a/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_conditions.go b/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_conditions.go index 927296be1ea92..a06dd097e299d 100644 --- a/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_conditions.go +++ b/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_conditions.go @@ -97,7 +97,7 @@ VALUES ( )`, pgx.NamedArgs{ "subscriptionID": subscriptionID, // Convert to string representation of EnterpriseSubscriptionCondition - "status": subscriptionsv1.EnterpriseSubscriptionCondition_Status_name[int32(opts.Status)], + "status": opts.Status.String(), "message": pointers.NilIfZero(opts.Message), "transitionTime": opts.TransitionTime, }) diff --git a/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_test.go b/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_test.go index e079859252725..1ecff4fe1f70d 100644 --- a/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_test.go +++ b/cmd/enterprise-portal/internal/database/subscriptions/subscriptions_test.go @@ -61,7 +61,7 @@ func SubscriptionsStoreList(t *testing.T, ctx context.Context, s *subscriptions. subscriptions.UpsertSubscriptionOptions{ DisplayName: database.NewNullString("Subscription 1"), InstanceDomain: database.NewNullString("s1.sourcegraph.com"), - SalesforceSubscriptionID: pointers.Ptr("sf_sub_id"), + SalesforceSubscriptionID: database.NewNullString("sf_sub_id"), }, ) require.NoError(t, err) @@ -199,6 +199,22 @@ func SubscriptionsStoreList(t *testing.T, ctx context.Context, s *subscriptions. assert.Equal(t, s1.ID, ss[0].ID) }) + t.Run("list by not archived", func(t *testing.T) { + t.Parallel() + + ss, err := s.List( + ctx, + subscriptions.ListEnterpriseSubscriptionsOptions{ + IsArchived: pointers.Ptr(false), + }, + ) + require.NoError(t, err) + assert.NotEmpty(t, ss) + for _, s := range ss { + assert.Nil(t, s.ArchivedAt) + } + }) + t.Run("list with page size", func(t *testing.T) { t.Parallel() diff --git a/cmd/enterprise-portal/internal/database/types.go b/cmd/enterprise-portal/internal/database/types.go index b0fb1e26ec95d..80578b4470abf 100644 --- a/cmd/enterprise-portal/internal/database/types.go +++ b/cmd/enterprise-portal/internal/database/types.go @@ -13,6 +13,17 @@ func NewNullString(v string) *sql.NullString { } } +// NewNullString creates an *sql.NullString that indicates "invalid", i.e. null, +// if v is nil or an empty string. It returns a pointer because many use cases +// require a pointer - it is safe to immediately deref the return value if you +// need to, since it always returns a non-nil value. +func NewNullStringPtr(v *string) *sql.NullString { + if v == nil { + return &sql.NullString{} + } + return NewNullString(*v) +} + // NewNullInt32 is like NewNullString, but always produces a valid value. func NewNullInt32[T int | int32 | int64 | uint64](v T) *sql.NullInt32 { return &sql.NullInt32{ diff --git a/cmd/enterprise-portal/internal/database/utctime/BUILD.bazel b/cmd/enterprise-portal/internal/database/utctime/BUILD.bazel index 0737791739283..a1ca885caa114 100644 --- a/cmd/enterprise-portal/internal/database/utctime/BUILD.bazel +++ b/cmd/enterprise-portal/internal/database/utctime/BUILD.bazel @@ -2,11 +2,15 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "utctime", - srcs = ["utctime.go"], + srcs = [ + "utctime.go", + "valast.go", + ], importpath = "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime", visibility = ["//cmd/enterprise-portal:__subpackages__"], deps = [ "//lib/errors", "//lib/pointers", + "@com_github_hexops_valast//:valast", ], ) diff --git a/cmd/enterprise-portal/internal/database/utctime/utctime.go b/cmd/enterprise-portal/internal/database/utctime/utctime.go index 0e90287687ea5..d454848d9fa23 100644 --- a/cmd/enterprise-portal/internal/database/utctime/utctime.go +++ b/cmd/enterprise-portal/internal/database/utctime/utctime.go @@ -29,6 +29,11 @@ func Now() Time { return Time(time.Now()) } // FromTime returns a utctime.Time from a time.Time. func FromTime(t time.Time) Time { return Time(t.UTC().Round(time.Microsecond)) } +// Date is analagous to time.Date, but only represents UTC time. +func Date(year int, month time.Month, day, hour, min, sec, nsec int) Time { + return FromTime(time.Date(year, month, day, hour, min, sec, nsec, time.UTC)) +} + var _ sql.Scanner = (*Time)(nil) func (t *Time) Scan(src any) error { diff --git a/cmd/enterprise-portal/internal/database/utctime/valast.go b/cmd/enterprise-portal/internal/database/utctime/valast.go new file mode 100644 index 0000000000000..f4f316f925816 --- /dev/null +++ b/cmd/enterprise-portal/internal/database/utctime/valast.go @@ -0,0 +1,31 @@ +package utctime + +import ( + "fmt" + "go/ast" + "go/token" + + "github.com/hexops/valast" +) + +// Register custom representation for autogold. +func init() { + valast.RegisterType(func(ut Time) ast.Expr { + t := ut.AsTime() + return &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "utctime"}, + Sel: &ast.Ident{Name: "Date"}, + }, + Args: []ast.Expr{ + &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Year())}, + &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Month())}, + &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Day())}, + &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Hour())}, + &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Minute())}, + &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Second())}, + &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", t.Nanosecond())}, + }, + } + }) +} diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel b/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel index d547fc7be1df7..d9d22945ab052 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel +++ b/cmd/enterprise-portal/internal/subscriptionsservice/BUILD.bazel @@ -16,9 +16,12 @@ go_library( "//cmd/enterprise-portal/internal/connectutil", "//cmd/enterprise-portal/internal/database", "//cmd/enterprise-portal/internal/database/subscriptions", + "//cmd/enterprise-portal/internal/database/utctime", "//cmd/enterprise-portal/internal/dotcomdb", "//cmd/enterprise-portal/internal/samsm2m", "//internal/collections", + "//internal/license", + "//internal/licensing", "//internal/trace", "//lib/enterpriseportal/subscriptions/v1:subscriptions", "//lib/enterpriseportal/subscriptions/v1/v1connect", @@ -26,11 +29,13 @@ go_library( "//lib/managedservicesplatform/iam", "//lib/pointers", "@com_connectrpc_connect//:connect", + "@com_github_google_uuid//:uuid", "@com_github_sourcegraph_log//:log", "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go", "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//clients/v1:clients", "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", "@org_golang_google_protobuf//types/known/timestamppb", + "@org_golang_x_crypto//ssh", "@org_golang_x_exp//maps", ], ) @@ -45,19 +50,28 @@ go_test( embed = [":subscriptionsservice"], deps = [ "//cmd/enterprise-portal/internal/database/subscriptions", + "//cmd/enterprise-portal/internal/database/utctime", "//cmd/enterprise-portal/internal/samsm2m", + "//internal/license", "//lib/enterpriseportal/subscriptions/v1:subscriptions", + "//lib/errors", "//lib/managedservicesplatform/iam", + "//lib/pointers", "@com_connectrpc_connect//:connect", "@com_github_derision_test_go_mockgen_v2//testutil/require", + "@com_github_google_uuid//:uuid", "@com_github_hexops_autogold_v2//:autogold", + "@com_github_hexops_valast//:valast", "@com_github_sourcegraph_log//logtest", "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//:sourcegraph-accounts-sdk-go", "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//clients/v1:clients", "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", + "@org_golang_google_protobuf//encoding/protojson", + "@org_golang_google_protobuf//reflect/protoreflect", "@org_golang_google_protobuf//types/known/fieldmaskpb", + "@org_golang_google_protobuf//types/known/timestamppb", ], ) diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/adapters.go b/cmd/enterprise-portal/internal/subscriptionsservice/adapters.go index 65bc0ca482d2e..8de1d2ad23a1d 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/adapters.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/adapters.go @@ -2,10 +2,16 @@ package subscriptionsservice import ( "encoding/json" + "fmt" + "strings" + "connectrpc.com/connect" "google.golang.org/protobuf/types/known/timestamppb" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime" + "github.com/sourcegraph/sourcegraph/internal/license" + "github.com/sourcegraph/sourcegraph/internal/licensing" subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/iam" @@ -34,7 +40,7 @@ func convertLicenseToProto(license *subscriptions.LicenseWithConditions) (*subsc case subscriptionsv1.EnterpriseSubscriptionLicenseType_ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY.String(): var data subscriptions.DataLicenseKey if err := json.Unmarshal(license.LicenseData, &data); err != nil { - return proto, errors.Wrap(err, "unmarshal license data") + return proto, errors.Wrapf(err, "unmarshal license data: %q", string(license.LicenseData)) } proto.License = &subscriptionsv1.EnterpriseSubscriptionLicense_Key{ Key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ @@ -46,9 +52,11 @@ func convertLicenseToProto(license *subscriptions.LicenseWithConditions) (*subsc SalesforceSubscriptionId: pointers.DerefZero(data.Info.SalesforceSubscriptionID), SalesforceOpportunityId: pointers.DerefZero(data.Info.SalesforceOpportunityID), }, - LicenseKey: data.SignedKey, + LicenseKey: data.SignedKey, + PlanDisplayName: licensing.ProductNameWithBrand(data.Info.Tags), }, } + default: return proto, errors.Newf("unknown license type %q", t) } @@ -105,8 +113,72 @@ func convertProtoToIAMTupleRelation(action subscriptionsv1.PermissionRelation) i func convertProtoRoleToIAMTupleObject(role subscriptionsv1.Role, subscriptionID string) iam.TupleObject { switch role { case subscriptionsv1.Role_ROLE_SUBSCRIPTION_CUSTOMER_ADMIN: - return iam.ToTupleObject(iam.TupleTypeCustomerAdmin, subscriptionID) + return iam.ToTupleObject(iam.TupleTypeCustomerAdmin, + strings.TrimPrefix(subscriptionID, subscriptionsv1.EnterpriseSubscriptionIDPrefix)) default: return "" } } + +// convertLicenseKeyToLicenseKeyData converts a create-license request into an +// actual license key for creating a database entry. +// +// It may return Connect errors - all other errors should be considered internal +// errors. +func convertLicenseKeyToLicenseKeyData( + createdAt utctime.Time, + sub *subscriptions.Subscription, + key *subscriptionsv1.EnterpriseSubscriptionLicenseKey, + // StoreV1.GetRequiredEnterpriseSubscriptionLicenseKeyTags + requiredTags []string, + // StoreV1.SignEnterpriseSubscriptionLicenseKey + signKeyFn func(license.Info) (string, error), +) (*subscriptions.DataLicenseKey, error) { + if key.GetInfo().GetUserCount() == 0 { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("user_count is invalid")) + } + expires := key.GetInfo().GetExpireTime().AsTime() + if expires.Before(createdAt.AsTime()) { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("expiry must be in the future")) + } + tags := key.GetInfo().GetTags() + providedTagPrefixes := map[string]struct{}{} + for _, t := range tags { + providedTagPrefixes[strings.SplitN(t, ":", 2)[0]] = struct{}{} + } + if _, exists := providedTagPrefixes["customer"]; !exists && sub.DisplayName != nil { + tags = append(tags, fmt.Sprintf("customer:%s", *sub.DisplayName)) + } + for _, r := range requiredTags { + if _, ok := providedTagPrefixes[r]; !ok { + return nil, connect.NewError(connect.CodeInvalidArgument, + errors.Newf("key tags [%s] are required", strings.Join(requiredTags, ", "))) + } + } + + info := license.Info{ + Tags: tags, + UserCount: uint(key.GetInfo().GetUserCount()), + CreatedAt: createdAt.AsTime(), + // Cast expiry to utctime and back for uniform representation + ExpiresAt: utctime.FromTime(expires).AsTime(), + // Provided at creation + SalesforceOpportunityID: pointers.NilIfZero(key.GetInfo().GetSalesforceOpportunityId()), + // Inherited from subscription + SalesforceSubscriptionID: sub.SalesforceSubscriptionID, + } + signedKey, err := signKeyFn(info) + if err != nil { + // See StoreV1.SignEnterpriseSubscriptionLicenseKey + if errors.Is(err, errStoreUnimplemented) { + return nil, connect.NewError(connect.CodeUnimplemented, + errors.Wrap(err, "key signing not available")) + } + return nil, errors.Wrap(err, "sign key") + } + + return &subscriptions.DataLicenseKey{ + Info: info, + SignedKey: signedKey, + }, nil +} diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/adapters_test.go b/cmd/enterprise-portal/internal/subscriptionsservice/adapters_test.go index 84ebc92986a78..5da4ef45cdca2 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/adapters_test.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/adapters_test.go @@ -1,13 +1,160 @@ package subscriptionsservice import ( + "encoding/json" + "fmt" "testing" + "time" + "github.com/hexops/autogold/v2" + "github.com/hexops/valast" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime" + "github.com/sourcegraph/sourcegraph/internal/license" subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1" + "github.com/sourcegraph/sourcegraph/lib/pointers" ) +// Protojson output isn't stable by injecting randomized whitespace, +// so we re-marshal it to stabilize the output for golden tests. +// https://github.com/golang/protobuf/issues/1082 +func mustMarshalStableProtoJSON(t *testing.T, m protoreflect.ProtoMessage) string { + t.Helper() + + protoJSON, err := protojson.Marshal(m) + require.NoError(t, err) + + var gotJSON map[string]any + require.NoError(t, json.Unmarshal(protoJSON, &gotJSON)) + return mustMarshal(json.MarshalIndent(gotJSON, "", " ")) +} + +func mustMarshal(d []byte, err error) string { + if err != nil { + return err.Error() + } + return string(d) +} + +func TestConvertLicenseToProto(t *testing.T) { + created := utctime.FromTime(newMockTime()) + expired := utctime.FromTime(newMockTime().Add(1 * time.Hour)) + got, err := convertLicenseToProto(&subscriptions.LicenseWithConditions{ + SubscriptionLicense: subscriptions.SubscriptionLicense{ + SubscriptionID: "subscription_id", + ID: "license_id", + CreatedAt: created, + ExpireAt: expired, + LicenseType: "ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY", + // In format subscriptions.DataLicenseKey + LicenseData: json.RawMessage(fmt.Sprintf(`{ + "SignedKey": "asdf", + "Info": {"e":%s,"c":%s,"t":["foo"]} + }`, + mustMarshal(expired.AsTime().MarshalJSON()), + mustMarshal(created.AsTime().MarshalJSON()))), + }, + Conditions: []subscriptions.SubscriptionLicenseCondition{{ + TransitionTime: created, + Status: "STATUS_CREATED", + }}, + }) + require.NoError(t, err) + + autogold.Expect(`{ + "conditions": [ + { + "lastTransitionTime": "2024-01-01T01:01:00Z", + "status": "STATUS_CREATED" + } + ], + "id": "esl_license_id", + "key": { + "info": { + "expireTime": "2024-01-01T02:01:00Z", + "tags": [ + "foo" + ] + }, + "infoVersion": 1, + "licenseKey": "asdf", + "planDisplayName": "Sourcegraph Enterprise" + }, + "subscriptionId": "es_subscription_id" +}`).Equal(t, mustMarshalStableProtoJSON(t, got)) +} + +func TestConvertSubscriptionToProto(t *testing.T) { + created := newMockTime() + for _, tc := range []struct { + name string + sub *subscriptions.SubscriptionWithConditions + want autogold.Value + }{{ + name: "without salesforce details", + sub: &subscriptions.SubscriptionWithConditions{ + Subscription: subscriptions.Subscription{ + ID: "subscription_id", + InstanceDomain: pointers.Ptr("sourcegraph.com"), + CreatedAt: utctime.Time(created), + }, + Conditions: []subscriptions.SubscriptionCondition{{ + TransitionTime: utctime.Time(created), + Status: "STATUS_CREATED", + }}, + }, + want: autogold.Expect(`{ + "conditions": [ + { + "lastTransitionTime": "2024-01-01T01:01:00Z", + "status": "STATUS_CREATED" + } + ], + "id": "es_subscription_id", + "instanceDomain": "sourcegraph.com" +}`), + }, { + name: "with salesforce details", + sub: &subscriptions.SubscriptionWithConditions{ + Subscription: subscriptions.Subscription{ + ID: "subscription_id", + DisplayName: pointers.Ptr("s2"), + CreatedAt: utctime.Time(created), + SalesforceSubscriptionID: pointers.Ptr("sf_sub_id"), + }, + Conditions: []subscriptions.SubscriptionCondition{{ + TransitionTime: utctime.Time(created), + Status: "STATUS_CREATED", + }}, + }, + want: autogold.Expect(`{ + "conditions": [ + { + "lastTransitionTime": "2024-01-01T01:01:00Z", + "status": "STATUS_CREATED" + } + ], + "displayName": "s2", + "id": "es_subscription_id", + "salesforce": { + "subscriptionId": "sf_sub_id" + } +}`), + }} { + t.Run(tc.name, func(t *testing.T) { + got := convertSubscriptionToProto(tc.sub) + + tc.want.Equal(t, mustMarshalStableProtoJSON(t, got)) + }) + } +} + func TestConvertProtoToIAMTupleObjectType(t *testing.T) { // Assert full coverage on API enum values. for tid, name := range subscriptionsv1.PermissionType_name { @@ -49,3 +196,196 @@ func TestConvertProtoRoleToIAMTupleObject(t *testing.T) { }) } } + +func TestConvertLicenseKeyToLicenseKeyData(t *testing.T) { + created := utctime.FromTime(newMockTime()) + for _, tc := range []struct { + name string + sub *subscriptions.Subscription + key *subscriptionsv1.EnterpriseSubscriptionLicenseKey + requiredTags []string + + wantError autogold.Value + wantData autogold.Value + }{{ + name: "invalid expiry", + sub: &subscriptions.Subscription{}, + key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + UserCount: 123, + ExpireTime: timestamppb.New(created.AsTime().Add(-time.Hour)), + }, + }, + wantError: autogold.Expect("invalid_argument: expiry must be in the future"), + }, { + name: "missing required tag", + sub: &subscriptions.Subscription{}, + key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + UserCount: 123, + ExpireTime: timestamppb.New(created.AsTime().Add(time.Hour)), + }, + }, + requiredTags: []string{"dev"}, + wantError: autogold.Expect("invalid_argument: key tags [dev] are required"), + }, { + name: "has required tag", + sub: &subscriptions.Subscription{}, + key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + UserCount: 123, + ExpireTime: timestamppb.New(created.AsTime().Add(time.Hour)), + Tags: []string{"dev", "plan"}, + }, + }, + requiredTags: []string{"dev"}, + wantData: autogold.Expect(&subscriptions.DataLicenseKey{ + Info: license.Info{ + Tags: []string{ + "dev", + "plan", + }, + UserCount: 123, + CreatedAt: time.Date(2024, + 1, + 1, + 1, + 1, + 0, + 0, + time.UTC), + ExpiresAt: time.Date(2024, + 1, + 1, + 2, + 1, + 0, + 0, + time.UTC), + }, + SignedKey: "signed-key", + }), + }, { + name: "adds display name as customer tag", + sub: &subscriptions.Subscription{ + DisplayName: pointers.Ptr(t.Name()), + }, + key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + UserCount: 123, + ExpireTime: timestamppb.New(created.AsTime().Add(time.Hour)), + }, + }, + wantData: autogold.Expect(&subscriptions.DataLicenseKey{ + Info: license.Info{ + Tags: []string{"customer:TestConvertLicenseKeyToLicenseKeyData"}, + UserCount: 123, + CreatedAt: time.Date(2024, + 1, + 1, + 1, + 1, + 0, + 0, + time.UTC), + ExpiresAt: time.Date(2024, + 1, + 1, + 2, + 1, + 0, + 0, + time.UTC), + }, + SignedKey: "signed-key", + }), + }, { + name: "respects existing customer tag", + sub: &subscriptions.Subscription{ + DisplayName: pointers.Ptr(t.Name()), + }, + key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + UserCount: 123, + ExpireTime: timestamppb.New(created.AsTime().Add(time.Hour)), + Tags: []string{"customer:custom-customer"}, + }, + }, + wantData: autogold.Expect(&subscriptions.DataLicenseKey{ + Info: license.Info{ + Tags: []string{"customer:custom-customer"}, + UserCount: 123, + CreatedAt: time.Date(2024, + 1, + 1, + 1, + 1, + 0, + 0, + time.UTC), + ExpiresAt: time.Date(2024, + 1, + 1, + 2, + 1, + 0, + 0, + time.UTC), + }, + SignedKey: "signed-key", + }), + }, { + name: "adds salesforce metadata", + sub: &subscriptions.Subscription{ + SalesforceSubscriptionID: pointers.Ptr("sf_sub_id"), + }, + key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + UserCount: 123, + ExpireTime: timestamppb.New(created.AsTime().Add(time.Hour)), + }, + }, + wantData: autogold.Expect(&subscriptions.DataLicenseKey{ + Info: license.Info{ + UserCount: 123, + CreatedAt: time.Date(2024, + 1, + 1, + 1, + 1, + 0, + 0, + time.UTC), + ExpiresAt: time.Date(2024, + 1, + 1, + 2, + 1, + 0, + 0, + time.UTC), + SalesforceSubscriptionID: valast.Ptr("sf_sub_id"), + }, + SignedKey: "signed-key", + }), + }} { + t.Run(tc.name, func(t *testing.T) { + got, err := convertLicenseKeyToLicenseKeyData( + created, + tc.sub, + tc.key, + tc.requiredTags, + func(i license.Info) (string, error) { + return "signed-key", nil + }, + ) + if tc.wantError != nil { + require.Error(t, err) + tc.wantError.Equal(t, err.Error()) + } else { + require.NoError(t, err) + tc.wantData.Equal(t, got) + } + }) + } +} diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go b/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go index 8121dfa547208..b3c864cc32746 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/mocks_test.go @@ -13,6 +13,8 @@ import ( sourcegraphaccountssdkgo "github.com/sourcegraph/sourcegraph-accounts-sdk-go" v1 "github.com/sourcegraph/sourcegraph-accounts-sdk-go/clients/v1" subscriptions "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions" + utctime "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime" + license "github.com/sourcegraph/sourcegraph/internal/license" iam "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/iam" ) @@ -21,6 +23,21 @@ import ( // github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/subscriptionsservice) // used for unit testing. type MockStoreV1 struct { + // CreateEnterpriseSubscriptionLicenseKeyFunc is an instance of a mock + // function object controlling the behavior of the method + // CreateEnterpriseSubscriptionLicenseKey. + CreateEnterpriseSubscriptionLicenseKeyFunc *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc + // GenerateSubscriptionIDFunc is an instance of a mock function object + // controlling the behavior of the method GenerateSubscriptionID. + GenerateSubscriptionIDFunc *StoreV1GenerateSubscriptionIDFunc + // GetEnterpriseSubscriptionFunc is an instance of a mock function + // object controlling the behavior of the method + // GetEnterpriseSubscription. + GetEnterpriseSubscriptionFunc *StoreV1GetEnterpriseSubscriptionFunc + // GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc is an instance of + // a mock function object controlling the behavior of the method + // GetRequiredEnterpriseSubscriptionLicenseKeyTags. + GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc // GetSAMSUserByIDFunc is an instance of a mock function object // controlling the behavior of the method GetSAMSUserByID. GetSAMSUserByIDFunc *StoreV1GetSAMSUserByIDFunc @@ -44,6 +61,17 @@ type MockStoreV1 struct { // object controlling the behavior of the method // ListEnterpriseSubscriptions. ListEnterpriseSubscriptionsFunc *StoreV1ListEnterpriseSubscriptionsFunc + // NowFunc is an instance of a mock function object controlling the + // behavior of the method Now. + NowFunc *StoreV1NowFunc + // RevokeEnterpriseSubscriptionLicenseFunc is an instance of a mock + // function object controlling the behavior of the method + // RevokeEnterpriseSubscriptionLicense. + RevokeEnterpriseSubscriptionLicenseFunc *StoreV1RevokeEnterpriseSubscriptionLicenseFunc + // SignEnterpriseSubscriptionLicenseKeyFunc is an instance of a mock + // function object controlling the behavior of the method + // SignEnterpriseSubscriptionLicenseKey. + SignEnterpriseSubscriptionLicenseKeyFunc *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc // UpsertEnterpriseSubscriptionFunc is an instance of a mock function // object controlling the behavior of the method // UpsertEnterpriseSubscription. @@ -54,6 +82,26 @@ type MockStoreV1 struct { // return zero values for all results, unless overwritten. func NewMockStoreV1() *MockStoreV1 { return &MockStoreV1{ + CreateEnterpriseSubscriptionLicenseKeyFunc: &StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc{ + defaultHook: func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (r0 *subscriptions.LicenseWithConditions, r1 error) { + return + }, + }, + GenerateSubscriptionIDFunc: &StoreV1GenerateSubscriptionIDFunc{ + defaultHook: func() (r0 string, r1 error) { + return + }, + }, + GetEnterpriseSubscriptionFunc: &StoreV1GetEnterpriseSubscriptionFunc{ + defaultHook: func(context.Context, string) (r0 *subscriptions.SubscriptionWithConditions, r1 error) { + return + }, + }, + GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc: &StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc{ + defaultHook: func() (r0 []string) { + return + }, + }, GetSAMSUserByIDFunc: &StoreV1GetSAMSUserByIDFunc{ defaultHook: func(context.Context, string) (r0 *v1.User, r1 error) { return @@ -89,8 +137,23 @@ func NewMockStoreV1() *MockStoreV1 { return }, }, + NowFunc: &StoreV1NowFunc{ + defaultHook: func() (r0 utctime.Time) { + return + }, + }, + RevokeEnterpriseSubscriptionLicenseFunc: &StoreV1RevokeEnterpriseSubscriptionLicenseFunc{ + defaultHook: func(context.Context, string, subscriptions.RevokeLicenseOpts) (r0 *subscriptions.LicenseWithConditions, r1 error) { + return + }, + }, + SignEnterpriseSubscriptionLicenseKeyFunc: &StoreV1SignEnterpriseSubscriptionLicenseKeyFunc{ + defaultHook: func(license.Info) (r0 string, r1 error) { + return + }, + }, UpsertEnterpriseSubscriptionFunc: &StoreV1UpsertEnterpriseSubscriptionFunc{ - defaultHook: func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (r0 *subscriptions.SubscriptionWithConditions, r1 error) { + defaultHook: func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (r0 *subscriptions.SubscriptionWithConditions, r1 error) { return }, }, @@ -101,6 +164,26 @@ func NewMockStoreV1() *MockStoreV1 { // methods panic on invocation, unless overwritten. func NewStrictMockStoreV1() *MockStoreV1 { return &MockStoreV1{ + CreateEnterpriseSubscriptionLicenseKeyFunc: &StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc{ + defaultHook: func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + panic("unexpected invocation of MockStoreV1.CreateEnterpriseSubscriptionLicenseKey") + }, + }, + GenerateSubscriptionIDFunc: &StoreV1GenerateSubscriptionIDFunc{ + defaultHook: func() (string, error) { + panic("unexpected invocation of MockStoreV1.GenerateSubscriptionID") + }, + }, + GetEnterpriseSubscriptionFunc: &StoreV1GetEnterpriseSubscriptionFunc{ + defaultHook: func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error) { + panic("unexpected invocation of MockStoreV1.GetEnterpriseSubscription") + }, + }, + GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc: &StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc{ + defaultHook: func() []string { + panic("unexpected invocation of MockStoreV1.GetRequiredEnterpriseSubscriptionLicenseKeyTags") + }, + }, GetSAMSUserByIDFunc: &StoreV1GetSAMSUserByIDFunc{ defaultHook: func(context.Context, string) (*v1.User, error) { panic("unexpected invocation of MockStoreV1.GetSAMSUserByID") @@ -136,8 +219,23 @@ func NewStrictMockStoreV1() *MockStoreV1 { panic("unexpected invocation of MockStoreV1.ListEnterpriseSubscriptions") }, }, + NowFunc: &StoreV1NowFunc{ + defaultHook: func() utctime.Time { + panic("unexpected invocation of MockStoreV1.Now") + }, + }, + RevokeEnterpriseSubscriptionLicenseFunc: &StoreV1RevokeEnterpriseSubscriptionLicenseFunc{ + defaultHook: func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + panic("unexpected invocation of MockStoreV1.RevokeEnterpriseSubscriptionLicense") + }, + }, + SignEnterpriseSubscriptionLicenseKeyFunc: &StoreV1SignEnterpriseSubscriptionLicenseKeyFunc{ + defaultHook: func(license.Info) (string, error) { + panic("unexpected invocation of MockStoreV1.SignEnterpriseSubscriptionLicenseKey") + }, + }, UpsertEnterpriseSubscriptionFunc: &StoreV1UpsertEnterpriseSubscriptionFunc{ - defaultHook: func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) { + defaultHook: func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { panic("unexpected invocation of MockStoreV1.UpsertEnterpriseSubscription") }, }, @@ -148,6 +246,18 @@ func NewStrictMockStoreV1() *MockStoreV1 { // methods delegate to the given implementation, unless overwritten. func NewMockStoreV1From(i StoreV1) *MockStoreV1 { return &MockStoreV1{ + CreateEnterpriseSubscriptionLicenseKeyFunc: &StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc{ + defaultHook: i.CreateEnterpriseSubscriptionLicenseKey, + }, + GenerateSubscriptionIDFunc: &StoreV1GenerateSubscriptionIDFunc{ + defaultHook: i.GenerateSubscriptionID, + }, + GetEnterpriseSubscriptionFunc: &StoreV1GetEnterpriseSubscriptionFunc{ + defaultHook: i.GetEnterpriseSubscription, + }, + GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc: &StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc{ + defaultHook: i.GetRequiredEnterpriseSubscriptionLicenseKeyTags, + }, GetSAMSUserByIDFunc: &StoreV1GetSAMSUserByIDFunc{ defaultHook: i.GetSAMSUserByID, }, @@ -169,12 +279,460 @@ func NewMockStoreV1From(i StoreV1) *MockStoreV1 { ListEnterpriseSubscriptionsFunc: &StoreV1ListEnterpriseSubscriptionsFunc{ defaultHook: i.ListEnterpriseSubscriptions, }, + NowFunc: &StoreV1NowFunc{ + defaultHook: i.Now, + }, + RevokeEnterpriseSubscriptionLicenseFunc: &StoreV1RevokeEnterpriseSubscriptionLicenseFunc{ + defaultHook: i.RevokeEnterpriseSubscriptionLicense, + }, + SignEnterpriseSubscriptionLicenseKeyFunc: &StoreV1SignEnterpriseSubscriptionLicenseKeyFunc{ + defaultHook: i.SignEnterpriseSubscriptionLicenseKey, + }, UpsertEnterpriseSubscriptionFunc: &StoreV1UpsertEnterpriseSubscriptionFunc{ defaultHook: i.UpsertEnterpriseSubscription, }, } } +// StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc describes the behavior +// when the CreateEnterpriseSubscriptionLicenseKey method of the parent +// MockStoreV1 instance is invoked. +type StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc struct { + defaultHook func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) + hooks []func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) + history []StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall + mutex sync.Mutex +} + +// CreateEnterpriseSubscriptionLicenseKey delegates to the next hook +// function in the queue and stores the parameter and result values of this +// invocation. +func (m *MockStoreV1) CreateEnterpriseSubscriptionLicenseKey(v0 context.Context, v1 string, v2 *subscriptions.DataLicenseKey, v3 subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + r0, r1 := m.CreateEnterpriseSubscriptionLicenseKeyFunc.nextHook()(v0, v1, v2, v3) + m.CreateEnterpriseSubscriptionLicenseKeyFunc.appendCall(StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall{v0, v1, v2, v3, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the +// CreateEnterpriseSubscriptionLicenseKey method of the parent MockStoreV1 +// instance is invoked and the hook queue is empty. +func (f *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc) SetDefaultHook(hook func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// CreateEnterpriseSubscriptionLicenseKey method of the parent MockStoreV1 +// instance invokes the hook at the front of the queue and discards it. +// After the queue is empty, the default hook function is invoked for any +// future action. +func (f *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc) PushHook(hook func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc) SetDefaultReturn(r0 *subscriptions.LicenseWithConditions, r1 error) { + f.SetDefaultHook(func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc) PushReturn(r0 *subscriptions.LicenseWithConditions, r1 error) { + f.PushHook(func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + return r0, r1 + }) +} + +func (f *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc) nextHook() func(context.Context, string, *subscriptions.DataLicenseKey, subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc) appendCall(r0 StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of +// StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall objects describing +// the invocations of this function. +func (f *StoreV1CreateEnterpriseSubscriptionLicenseKeyFunc) History() []StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall { + f.mutex.Lock() + history := make([]StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall is an object that +// describes an invocation of method CreateEnterpriseSubscriptionLicenseKey +// on an instance of MockStoreV1. +type StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 string + // Arg2 is the value of the 3rd argument passed to this method + // invocation. + Arg2 *subscriptions.DataLicenseKey + // Arg3 is the value of the 4th argument passed to this method + // invocation. + Arg3 subscriptions.CreateLicenseOpts + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 *subscriptions.LicenseWithConditions + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreV1CreateEnterpriseSubscriptionLicenseKeyFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// StoreV1GenerateSubscriptionIDFunc describes the behavior when the +// GenerateSubscriptionID method of the parent MockStoreV1 instance is +// invoked. +type StoreV1GenerateSubscriptionIDFunc struct { + defaultHook func() (string, error) + hooks []func() (string, error) + history []StoreV1GenerateSubscriptionIDFuncCall + mutex sync.Mutex +} + +// GenerateSubscriptionID delegates to the next hook function in the queue +// and stores the parameter and result values of this invocation. +func (m *MockStoreV1) GenerateSubscriptionID() (string, error) { + r0, r1 := m.GenerateSubscriptionIDFunc.nextHook()() + m.GenerateSubscriptionIDFunc.appendCall(StoreV1GenerateSubscriptionIDFuncCall{r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the +// GenerateSubscriptionID method of the parent MockStoreV1 instance is +// invoked and the hook queue is empty. +func (f *StoreV1GenerateSubscriptionIDFunc) SetDefaultHook(hook func() (string, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GenerateSubscriptionID method of the parent MockStoreV1 instance invokes +// the hook at the front of the queue and discards it. After the queue is +// empty, the default hook function is invoked for any future action. +func (f *StoreV1GenerateSubscriptionIDFunc) PushHook(hook func() (string, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreV1GenerateSubscriptionIDFunc) SetDefaultReturn(r0 string, r1 error) { + f.SetDefaultHook(func() (string, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreV1GenerateSubscriptionIDFunc) PushReturn(r0 string, r1 error) { + f.PushHook(func() (string, error) { + return r0, r1 + }) +} + +func (f *StoreV1GenerateSubscriptionIDFunc) nextHook() func() (string, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreV1GenerateSubscriptionIDFunc) appendCall(r0 StoreV1GenerateSubscriptionIDFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of StoreV1GenerateSubscriptionIDFuncCall +// objects describing the invocations of this function. +func (f *StoreV1GenerateSubscriptionIDFunc) History() []StoreV1GenerateSubscriptionIDFuncCall { + f.mutex.Lock() + history := make([]StoreV1GenerateSubscriptionIDFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreV1GenerateSubscriptionIDFuncCall is an object that describes an +// invocation of method GenerateSubscriptionID on an instance of +// MockStoreV1. +type StoreV1GenerateSubscriptionIDFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 string + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreV1GenerateSubscriptionIDFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreV1GenerateSubscriptionIDFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// StoreV1GetEnterpriseSubscriptionFunc describes the behavior when the +// GetEnterpriseSubscription method of the parent MockStoreV1 instance is +// invoked. +type StoreV1GetEnterpriseSubscriptionFunc struct { + defaultHook func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error) + hooks []func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error) + history []StoreV1GetEnterpriseSubscriptionFuncCall + mutex sync.Mutex +} + +// GetEnterpriseSubscription delegates to the next hook function in the +// queue and stores the parameter and result values of this invocation. +func (m *MockStoreV1) GetEnterpriseSubscription(v0 context.Context, v1 string) (*subscriptions.SubscriptionWithConditions, error) { + r0, r1 := m.GetEnterpriseSubscriptionFunc.nextHook()(v0, v1) + m.GetEnterpriseSubscriptionFunc.appendCall(StoreV1GetEnterpriseSubscriptionFuncCall{v0, v1, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the +// GetEnterpriseSubscription method of the parent MockStoreV1 instance is +// invoked and the hook queue is empty. +func (f *StoreV1GetEnterpriseSubscriptionFunc) SetDefaultHook(hook func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GetEnterpriseSubscription method of the parent MockStoreV1 instance +// invokes the hook at the front of the queue and discards it. After the +// queue is empty, the default hook function is invoked for any future +// action. +func (f *StoreV1GetEnterpriseSubscriptionFunc) PushHook(hook func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreV1GetEnterpriseSubscriptionFunc) SetDefaultReturn(r0 *subscriptions.SubscriptionWithConditions, r1 error) { + f.SetDefaultHook(func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreV1GetEnterpriseSubscriptionFunc) PushReturn(r0 *subscriptions.SubscriptionWithConditions, r1 error) { + f.PushHook(func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error) { + return r0, r1 + }) +} + +func (f *StoreV1GetEnterpriseSubscriptionFunc) nextHook() func(context.Context, string) (*subscriptions.SubscriptionWithConditions, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreV1GetEnterpriseSubscriptionFunc) appendCall(r0 StoreV1GetEnterpriseSubscriptionFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of StoreV1GetEnterpriseSubscriptionFuncCall +// objects describing the invocations of this function. +func (f *StoreV1GetEnterpriseSubscriptionFunc) History() []StoreV1GetEnterpriseSubscriptionFuncCall { + f.mutex.Lock() + history := make([]StoreV1GetEnterpriseSubscriptionFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreV1GetEnterpriseSubscriptionFuncCall is an object that describes an +// invocation of method GetEnterpriseSubscription on an instance of +// MockStoreV1. +type StoreV1GetEnterpriseSubscriptionFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 string + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 *subscriptions.SubscriptionWithConditions + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreV1GetEnterpriseSubscriptionFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreV1GetEnterpriseSubscriptionFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc describes the +// behavior when the GetRequiredEnterpriseSubscriptionLicenseKeyTags method +// of the parent MockStoreV1 instance is invoked. +type StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc struct { + defaultHook func() []string + hooks []func() []string + history []StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall + mutex sync.Mutex +} + +// GetRequiredEnterpriseSubscriptionLicenseKeyTags delegates to the next +// hook function in the queue and stores the parameter and result values of +// this invocation. +func (m *MockStoreV1) GetRequiredEnterpriseSubscriptionLicenseKeyTags() []string { + r0 := m.GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc.nextHook()() + m.GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc.appendCall(StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the +// GetRequiredEnterpriseSubscriptionLicenseKeyTags method of the parent +// MockStoreV1 instance is invoked and the hook queue is empty. +func (f *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc) SetDefaultHook(hook func() []string) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// GetRequiredEnterpriseSubscriptionLicenseKeyTags method of the parent +// MockStoreV1 instance invokes the hook at the front of the queue and +// discards it. After the queue is empty, the default hook function is +// invoked for any future action. +func (f *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc) PushHook(hook func() []string) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc) SetDefaultReturn(r0 []string) { + f.SetDefaultHook(func() []string { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc) PushReturn(r0 []string) { + f.PushHook(func() []string { + return r0 + }) +} + +func (f *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc) nextHook() func() []string { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc) appendCall(r0 StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of +// StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall objects +// describing the invocations of this function. +func (f *StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc) History() []StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall { + f.mutex.Lock() + history := make([]StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall is an +// object that describes an invocation of method +// GetRequiredEnterpriseSubscriptionLicenseKeyTags on an instance of +// MockStoreV1. +type StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 []string +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreV1GetRequiredEnterpriseSubscriptionLicenseKeyTagsFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + // StoreV1GetSAMSUserByIDFunc describes the behavior when the // GetSAMSUserByID method of the parent MockStoreV1 instance is invoked. type StoreV1GetSAMSUserByIDFunc struct { @@ -933,28 +1491,352 @@ func (c StoreV1ListEnterpriseSubscriptionsFuncCall) Results() []interface{} { return []interface{}{c.Result0, c.Result1} } +// StoreV1NowFunc describes the behavior when the Now method of the parent +// MockStoreV1 instance is invoked. +type StoreV1NowFunc struct { + defaultHook func() utctime.Time + hooks []func() utctime.Time + history []StoreV1NowFuncCall + mutex sync.Mutex +} + +// Now delegates to the next hook function in the queue and stores the +// parameter and result values of this invocation. +func (m *MockStoreV1) Now() utctime.Time { + r0 := m.NowFunc.nextHook()() + m.NowFunc.appendCall(StoreV1NowFuncCall{r0}) + return r0 +} + +// SetDefaultHook sets function that is called when the Now method of the +// parent MockStoreV1 instance is invoked and the hook queue is empty. +func (f *StoreV1NowFunc) SetDefaultHook(hook func() utctime.Time) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// Now method of the parent MockStoreV1 instance invokes the hook at the +// front of the queue and discards it. After the queue is empty, the default +// hook function is invoked for any future action. +func (f *StoreV1NowFunc) PushHook(hook func() utctime.Time) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreV1NowFunc) SetDefaultReturn(r0 utctime.Time) { + f.SetDefaultHook(func() utctime.Time { + return r0 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreV1NowFunc) PushReturn(r0 utctime.Time) { + f.PushHook(func() utctime.Time { + return r0 + }) +} + +func (f *StoreV1NowFunc) nextHook() func() utctime.Time { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreV1NowFunc) appendCall(r0 StoreV1NowFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of StoreV1NowFuncCall objects describing the +// invocations of this function. +func (f *StoreV1NowFunc) History() []StoreV1NowFuncCall { + f.mutex.Lock() + history := make([]StoreV1NowFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreV1NowFuncCall is an object that describes an invocation of method +// Now on an instance of MockStoreV1. +type StoreV1NowFuncCall struct { + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 utctime.Time +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreV1NowFuncCall) Args() []interface{} { + return []interface{}{} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreV1NowFuncCall) Results() []interface{} { + return []interface{}{c.Result0} +} + +// StoreV1RevokeEnterpriseSubscriptionLicenseFunc describes the behavior +// when the RevokeEnterpriseSubscriptionLicense method of the parent +// MockStoreV1 instance is invoked. +type StoreV1RevokeEnterpriseSubscriptionLicenseFunc struct { + defaultHook func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) + hooks []func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) + history []StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall + mutex sync.Mutex +} + +// RevokeEnterpriseSubscriptionLicense delegates to the next hook function +// in the queue and stores the parameter and result values of this +// invocation. +func (m *MockStoreV1) RevokeEnterpriseSubscriptionLicense(v0 context.Context, v1 string, v2 subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + r0, r1 := m.RevokeEnterpriseSubscriptionLicenseFunc.nextHook()(v0, v1, v2) + m.RevokeEnterpriseSubscriptionLicenseFunc.appendCall(StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall{v0, v1, v2, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the +// RevokeEnterpriseSubscriptionLicense method of the parent MockStoreV1 +// instance is invoked and the hook queue is empty. +func (f *StoreV1RevokeEnterpriseSubscriptionLicenseFunc) SetDefaultHook(hook func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// RevokeEnterpriseSubscriptionLicense method of the parent MockStoreV1 +// instance invokes the hook at the front of the queue and discards it. +// After the queue is empty, the default hook function is invoked for any +// future action. +func (f *StoreV1RevokeEnterpriseSubscriptionLicenseFunc) PushHook(hook func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreV1RevokeEnterpriseSubscriptionLicenseFunc) SetDefaultReturn(r0 *subscriptions.LicenseWithConditions, r1 error) { + f.SetDefaultHook(func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreV1RevokeEnterpriseSubscriptionLicenseFunc) PushReturn(r0 *subscriptions.LicenseWithConditions, r1 error) { + f.PushHook(func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + return r0, r1 + }) +} + +func (f *StoreV1RevokeEnterpriseSubscriptionLicenseFunc) nextHook() func(context.Context, string, subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreV1RevokeEnterpriseSubscriptionLicenseFunc) appendCall(r0 StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of +// StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall objects describing the +// invocations of this function. +func (f *StoreV1RevokeEnterpriseSubscriptionLicenseFunc) History() []StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall { + f.mutex.Lock() + history := make([]StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall is an object that +// describes an invocation of method RevokeEnterpriseSubscriptionLicense on +// an instance of MockStoreV1. +type StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 context.Context + // Arg1 is the value of the 2nd argument passed to this method + // invocation. + Arg1 string + // Arg2 is the value of the 3rd argument passed to this method + // invocation. + Arg2 subscriptions.RevokeLicenseOpts + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 *subscriptions.LicenseWithConditions + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall) Args() []interface{} { + return []interface{}{c.Arg0, c.Arg1, c.Arg2} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreV1RevokeEnterpriseSubscriptionLicenseFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + +// StoreV1SignEnterpriseSubscriptionLicenseKeyFunc describes the behavior +// when the SignEnterpriseSubscriptionLicenseKey method of the parent +// MockStoreV1 instance is invoked. +type StoreV1SignEnterpriseSubscriptionLicenseKeyFunc struct { + defaultHook func(license.Info) (string, error) + hooks []func(license.Info) (string, error) + history []StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall + mutex sync.Mutex +} + +// SignEnterpriseSubscriptionLicenseKey delegates to the next hook function +// in the queue and stores the parameter and result values of this +// invocation. +func (m *MockStoreV1) SignEnterpriseSubscriptionLicenseKey(v0 license.Info) (string, error) { + r0, r1 := m.SignEnterpriseSubscriptionLicenseKeyFunc.nextHook()(v0) + m.SignEnterpriseSubscriptionLicenseKeyFunc.appendCall(StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall{v0, r0, r1}) + return r0, r1 +} + +// SetDefaultHook sets function that is called when the +// SignEnterpriseSubscriptionLicenseKey method of the parent MockStoreV1 +// instance is invoked and the hook queue is empty. +func (f *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc) SetDefaultHook(hook func(license.Info) (string, error)) { + f.defaultHook = hook +} + +// PushHook adds a function to the end of hook queue. Each invocation of the +// SignEnterpriseSubscriptionLicenseKey method of the parent MockStoreV1 +// instance invokes the hook at the front of the queue and discards it. +// After the queue is empty, the default hook function is invoked for any +// future action. +func (f *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc) PushHook(hook func(license.Info) (string, error)) { + f.mutex.Lock() + f.hooks = append(f.hooks, hook) + f.mutex.Unlock() +} + +// SetDefaultReturn calls SetDefaultHook with a function that returns the +// given values. +func (f *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc) SetDefaultReturn(r0 string, r1 error) { + f.SetDefaultHook(func(license.Info) (string, error) { + return r0, r1 + }) +} + +// PushReturn calls PushHook with a function that returns the given values. +func (f *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc) PushReturn(r0 string, r1 error) { + f.PushHook(func(license.Info) (string, error) { + return r0, r1 + }) +} + +func (f *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc) nextHook() func(license.Info) (string, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + if len(f.hooks) == 0 { + return f.defaultHook + } + + hook := f.hooks[0] + f.hooks = f.hooks[1:] + return hook +} + +func (f *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc) appendCall(r0 StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall) { + f.mutex.Lock() + f.history = append(f.history, r0) + f.mutex.Unlock() +} + +// History returns a sequence of +// StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall objects describing +// the invocations of this function. +func (f *StoreV1SignEnterpriseSubscriptionLicenseKeyFunc) History() []StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall { + f.mutex.Lock() + history := make([]StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall, len(f.history)) + copy(history, f.history) + f.mutex.Unlock() + + return history +} + +// StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall is an object that +// describes an invocation of method SignEnterpriseSubscriptionLicenseKey on +// an instance of MockStoreV1. +type StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall struct { + // Arg0 is the value of the 1st argument passed to this method + // invocation. + Arg0 license.Info + // Result0 is the value of the 1st result returned from this method + // invocation. + Result0 string + // Result1 is the value of the 2nd result returned from this method + // invocation. + Result1 error +} + +// Args returns an interface slice containing the arguments of this +// invocation. +func (c StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall) Args() []interface{} { + return []interface{}{c.Arg0} +} + +// Results returns an interface slice containing the results of this +// invocation. +func (c StoreV1SignEnterpriseSubscriptionLicenseKeyFuncCall) Results() []interface{} { + return []interface{}{c.Result0, c.Result1} +} + // StoreV1UpsertEnterpriseSubscriptionFunc describes the behavior when the // UpsertEnterpriseSubscription method of the parent MockStoreV1 instance is // invoked. type StoreV1UpsertEnterpriseSubscriptionFunc struct { - defaultHook func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) - hooks []func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) + defaultHook func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) + hooks []func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) history []StoreV1UpsertEnterpriseSubscriptionFuncCall mutex sync.Mutex } // UpsertEnterpriseSubscription delegates to the next hook function in the // queue and stores the parameter and result values of this invocation. -func (m *MockStoreV1) UpsertEnterpriseSubscription(v0 context.Context, v1 string, v2 subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) { - r0, r1 := m.UpsertEnterpriseSubscriptionFunc.nextHook()(v0, v1, v2) - m.UpsertEnterpriseSubscriptionFunc.appendCall(StoreV1UpsertEnterpriseSubscriptionFuncCall{v0, v1, v2, r0, r1}) +func (m *MockStoreV1) UpsertEnterpriseSubscription(v0 context.Context, v1 string, v2 subscriptions.UpsertSubscriptionOptions, v3 ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { + r0, r1 := m.UpsertEnterpriseSubscriptionFunc.nextHook()(v0, v1, v2, v3...) + m.UpsertEnterpriseSubscriptionFunc.appendCall(StoreV1UpsertEnterpriseSubscriptionFuncCall{v0, v1, v2, v3, r0, r1}) return r0, r1 } // SetDefaultHook sets function that is called when the // UpsertEnterpriseSubscription method of the parent MockStoreV1 instance is // invoked and the hook queue is empty. -func (f *StoreV1UpsertEnterpriseSubscriptionFunc) SetDefaultHook(hook func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error)) { +func (f *StoreV1UpsertEnterpriseSubscriptionFunc) SetDefaultHook(hook func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error)) { f.defaultHook = hook } @@ -963,7 +1845,7 @@ func (f *StoreV1UpsertEnterpriseSubscriptionFunc) SetDefaultHook(hook func(conte // invokes the hook at the front of the queue and discards it. After the // queue is empty, the default hook function is invoked for any future // action. -func (f *StoreV1UpsertEnterpriseSubscriptionFunc) PushHook(hook func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error)) { +func (f *StoreV1UpsertEnterpriseSubscriptionFunc) PushHook(hook func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error)) { f.mutex.Lock() f.hooks = append(f.hooks, hook) f.mutex.Unlock() @@ -972,19 +1854,19 @@ func (f *StoreV1UpsertEnterpriseSubscriptionFunc) PushHook(hook func(context.Con // SetDefaultReturn calls SetDefaultHook with a function that returns the // given values. func (f *StoreV1UpsertEnterpriseSubscriptionFunc) SetDefaultReturn(r0 *subscriptions.SubscriptionWithConditions, r1 error) { - f.SetDefaultHook(func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) { + f.SetDefaultHook(func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { return r0, r1 }) } // PushReturn calls PushHook with a function that returns the given values. func (f *StoreV1UpsertEnterpriseSubscriptionFunc) PushReturn(r0 *subscriptions.SubscriptionWithConditions, r1 error) { - f.PushHook(func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) { + f.PushHook(func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { return r0, r1 }) } -func (f *StoreV1UpsertEnterpriseSubscriptionFunc) nextHook() func(context.Context, string, subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) { +func (f *StoreV1UpsertEnterpriseSubscriptionFunc) nextHook() func(context.Context, string, subscriptions.UpsertSubscriptionOptions, ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { f.mutex.Lock() defer f.mutex.Unlock() @@ -1027,6 +1909,9 @@ type StoreV1UpsertEnterpriseSubscriptionFuncCall struct { // Arg2 is the value of the 3rd argument passed to this method // invocation. Arg2 subscriptions.UpsertSubscriptionOptions + // Arg3 is a slice containing the values of the variadic arguments + // passed to this method invocation. + Arg3 []subscriptions.CreateSubscriptionConditionOptions // Result0 is the value of the 1st result returned from this method // invocation. Result0 *subscriptions.SubscriptionWithConditions @@ -1036,9 +1921,16 @@ type StoreV1UpsertEnterpriseSubscriptionFuncCall struct { } // Args returns an interface slice containing the arguments of this -// invocation. +// invocation. The variadic slice argument is flattened in this array such +// that one positional argument and three variadic arguments would result in +// a slice of four, not two. func (c StoreV1UpsertEnterpriseSubscriptionFuncCall) Args() []interface{} { - return []interface{}{c.Arg0, c.Arg1, c.Arg2} + trailing := []interface{}{} + for _, val := range c.Arg3 { + trailing = append(trailing, val) + } + + return append([]interface{}{c.Arg0, c.Arg1, c.Arg2}, trailing...) } // Results returns an interface slice containing the results of this diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/v1.go b/cmd/enterprise-portal/internal/subscriptionsservice/v1.go index dc13f638a0144..a9ceccc9fb42c 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/v1.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/v1.go @@ -2,6 +2,7 @@ package subscriptionsservice import ( "context" + "fmt" "net/http" "strings" @@ -16,10 +17,12 @@ import ( subscriptionsv1connect "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1/v1connect" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/iam" + "github.com/sourcegraph/sourcegraph/lib/pointers" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/connectutil" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/dotcomdb" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/samsm2m" "github.com/sourcegraph/sourcegraph/internal/collections" @@ -46,19 +49,51 @@ func RegisterV1( } type handlerV1 struct { - subscriptionsv1connect.UnimplementedSubscriptionsServiceHandler - logger log.Logger store StoreV1 } var _ subscriptionsv1connect.SubscriptionsServiceHandler = (*handlerV1)(nil) +func (s *handlerV1) GetEnterpriseSubscription(ctx context.Context, req *connect.Request[subscriptionsv1.GetEnterpriseSubscriptionRequest]) (*connect.Response[subscriptionsv1.GetEnterpriseSubscriptionResponse], error) { + logger := trace.Logger(ctx, s.logger) + + // 🚨 SECURITY: Require appropriate M2M scope. + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, scopes.ActionRead) + clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) + if err != nil { + return nil, err + } + logger = logger.With(clientAttrs...) + + subscriptionID := req.Msg.GetId() + if subscriptionID == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("subscription_id is required")) + } + + sub, err := s.store.GetEnterpriseSubscription(ctx, subscriptionID) + if err != nil { + if errors.Is(err, subscriptions.ErrSubscriptionNotFound) { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to find subscription") + } + + proto := convertSubscriptionToProto(sub) + logger.Scoped("audit").Info("GetEnterpriseSubscription", + log.String("subscription", proto.Id)) + return connect.NewResponse(&subscriptionsv1.GetEnterpriseSubscriptionResponse{ + Subscription: proto, + }), nil +} + func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connect.Request[subscriptionsv1.ListEnterpriseSubscriptionsRequest]) (*connect.Response[subscriptionsv1.ListEnterpriseSubscriptionsResponse], error) { logger := trace.Logger(ctx, s.logger) // 🚨 SECURITY: Require appropriate M2M scope. - requiredScope := samsm2m.EnterprisePortalScope("subscription", scopes.ActionRead) + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, scopes.ActionRead) clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) if err != nil { return nil, err @@ -76,7 +111,7 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec filters := req.Msg.GetFilters() var ( isArchived *bool - subscriptionIDs = make(collections.Set[string], len(filters)) + internalSubscriptionIDs = make(collections.Set[string], len(filters)) displayNameSubstring string salesforceSubscriptionIDs []string instanceDomains []string @@ -91,7 +126,7 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec errors.New(`invalid filter: "subscription_id" provided but is empty`), ) } - subscriptionIDs.Add( + internalSubscriptionIDs.Add( strings.TrimPrefix(f.SubscriptionId, subscriptionsv1.EnterpriseSubscriptionIDPrefix)) case *subscriptionsv1.ListEnterpriseSubscriptionsFilter_IsArchived: isArchived = &f.IsArchived @@ -184,18 +219,18 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec allowedSubscriptionIDs.Add(strings.TrimPrefix(objectID, "subscription_cody_analytics:")) } - if !subscriptionIDs.IsEmpty() { + if !internalSubscriptionIDs.IsEmpty() { // If subscription IDs were provided, we only want to return the // subscriptions that are part of the provided IDs. - subscriptionIDs = collections.Intersection(subscriptionIDs, allowedSubscriptionIDs) + internalSubscriptionIDs = collections.Intersection(internalSubscriptionIDs, allowedSubscriptionIDs) } else { // Otherwise, only return the allowed subscriptions. - subscriptionIDs = allowedSubscriptionIDs + internalSubscriptionIDs = allowedSubscriptionIDs } // 🚨 SECURITY: If permissions are used as filter, but we found no results, we // should directly return an empty response to not mistaken as list all. - if len(subscriptionIDs) == 0 { + if len(internalSubscriptionIDs) == 0 { return connect.NewResponse(&subscriptionsv1.ListEnterpriseSubscriptionsResponse{}), nil } } @@ -203,7 +238,7 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec subs, err := s.store.ListEnterpriseSubscriptions( ctx, subscriptions.ListEnterpriseSubscriptionsOptions{ - IDs: subscriptionIDs.Values(), + IDs: internalSubscriptionIDs.Values(), IsArchived: isArchived, InstanceDomains: instanceDomains, DisplayNameSubstring: displayNameSubstring, @@ -240,7 +275,8 @@ func (s *handlerV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, req logger := trace.Logger(ctx, s.logger) // 🚨 SECURITY: Require appropriate M2M scope. - requiredScope := samsm2m.EnterprisePortalScope("subscription", scopes.ActionRead) + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, scopes.ActionRead) clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) if err != nil { return nil, err @@ -288,7 +324,7 @@ func (s *handlerV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, req if opts.LicenseKeySubstring != "" { return nil, connect.NewError( connect.CodeInvalidArgument, - errors.New(`invalid filter: "license_key_substring"" provided multiple times`), + errors.New(`invalid filter: "license_key_substring" provided multiple times`), ) } opts.LicenseKeySubstring = f.LicenseKeySubstring @@ -297,13 +333,13 @@ func (s *handlerV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, req if f.SubscriptionId == "" { return nil, connect.NewError( connect.CodeInvalidArgument, - errors.New(`invalid filter: "subscription_id"" provided but is empty`), + errors.New(`invalid filter: "subscription_id" provided but is empty`), ) } if opts.SubscriptionID != "" { return nil, connect.NewError( connect.CodeInvalidArgument, - errors.New(`invalid filter: "subscription_id"" provided multiple times`), + errors.New(`invalid filter: "subscription_id" provided multiple times`), ) } opts.SubscriptionID = f.SubscriptionId @@ -355,7 +391,8 @@ func (s *handlerV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, req for i, l := range licenses { resp.Licenses[i], err = convertLicenseToProto(l) if err != nil { - return nil, connectutil.InternalError(ctx, logger, err, + return nil, connectutil.InternalError(ctx, logger, + errors.Wrap(err, l.ID), "failed to read Enterprise Subscription license") } accessedSubscriptions[resp.Licenses[i].GetSubscriptionId()] = struct{}{} @@ -368,22 +405,100 @@ func (s *handlerV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, req return connect.NewResponse(&resp), nil } +func (s *handlerV1) CreateEnterpriseSubscription(ctx context.Context, req *connect.Request[subscriptionsv1.CreateEnterpriseSubscriptionRequest]) (*connect.Response[subscriptionsv1.CreateEnterpriseSubscriptionResponse], error) { + logger := trace.Logger(ctx, s.logger) + + // 🚨 SECURITY: Require appropriate M2M scope. + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, scopes.ActionWrite) + clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) + if err != nil { + return nil, err + } + logger = logger.With(clientAttrs...) + + sub := req.Msg.GetSubscription() + if sub == nil { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("subscription details are required")) + } + + // Validate required arguments. + if strings.TrimSpace(sub.GetDisplayName()) == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("display_name is required")) + } + + // Generate a new ID for the subscription. + if sub.Id != "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("subscription_id can not be set")) + } + sub.Id, err = s.store.GenerateSubscriptionID() + if err != nil { + return nil, connectutil.InternalError(ctx, s.logger, err, "failed to generate new subscription ID") + } + + // Check for an existing subscription, just in case. + if _, err := s.store.GetEnterpriseSubscription(ctx, sub.Id); err == nil { + return nil, connect.NewError(connect.CodeAlreadyExists, err) + } else if !errors.Is(err, subscriptions.ErrSubscriptionNotFound) { + return nil, connectutil.InternalError(ctx, logger, err, + "failed to check for existing subscription") + } + + createdAt := s.store.Now() + createdSub, err := s.store.UpsertEnterpriseSubscription(ctx, sub.Id, + subscriptions.UpsertSubscriptionOptions{ + CreatedAt: createdAt, + DisplayName: database.NewNullString(sub.GetDisplayName()), + InstanceDomain: database.NewNullString(sub.GetInstanceDomain()), + SalesforceSubscriptionID: database.NewNullString(sub.GetSalesforce().GetSubscriptionId()), + }, + subscriptions.CreateSubscriptionConditionOptions{ + Status: subscriptionsv1.EnterpriseSubscriptionCondition_STATUS_CREATED, + TransitionTime: createdAt, + Message: req.Msg.GetMessage(), + }) + if err != nil { + if errors.Is(err, subscriptions.ErrInvalidArgument) { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to create subscription") + } + + protoSub := convertSubscriptionToProto(createdSub) + logger.Scoped("audit").Info("CreateEnterpriseSubscription", + log.String("createdSubscription", protoSub.GetId())) + return connect.NewResponse(&subscriptionsv1.CreateEnterpriseSubscriptionResponse{ + Subscription: protoSub, + }), nil +} + func (s *handlerV1) UpdateEnterpriseSubscription(ctx context.Context, req *connect.Request[subscriptionsv1.UpdateEnterpriseSubscriptionRequest]) (*connect.Response[subscriptionsv1.UpdateEnterpriseSubscriptionResponse], error) { logger := trace.Logger(ctx, s.logger) // 🚨 SECURITY: Require appropriate M2M scope. - requiredScope := samsm2m.EnterprisePortalScope("subscription", scopes.ActionWrite) + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, scopes.ActionWrite) clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) if err != nil { return nil, err } logger = logger.With(clientAttrs...) - subscriptionID := strings.TrimPrefix(req.Msg.GetSubscription().GetId(), subscriptionsv1.EnterpriseSubscriptionIDPrefix) + subscriptionID := req.Msg.GetSubscription().GetId() if subscriptionID == "" { return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("subscription.id is required")) } + if existing, err := s.store.GetEnterpriseSubscription(ctx, subscriptionID); err != nil { + if errors.Is(err, subscriptions.ErrSubscriptionNotFound) { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to find subscription") + } else if existing.ArchivedAt != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, + errors.New("archived subscriptions cannot be updated")) + } + var opts subscriptions.UpsertSubscriptionOptions fieldPaths := req.Msg.GetUpdateMask().GetPaths() @@ -395,22 +510,33 @@ func (s *handlerV1) UpdateEnterpriseSubscription(ctx context.Context, req *conne if v := req.Msg.GetSubscription().GetDisplayName(); v != "" { opts.DisplayName = database.NewNullString(v) } + if v := req.Msg.GetSubscription().GetSalesforce().GetSubscriptionId(); v != "" { + opts.SalesforceSubscriptionID = database.NewNullString(v) + } } else { for _, p := range fieldPaths { - switch p { - case "instance_domain": - opts.InstanceDomain = - database.NewNullString(req.Msg.GetSubscription().GetInstanceDomain()) - case "display_name": - opts.DisplayName = - database.NewNullString(req.Msg.GetSubscription().GetDisplayName()) - case "*": + var valid bool + if p == "*" { + valid = true opts.ForceUpdate = true + } + if p == "instance_domain" || p == "*" { + valid = true opts.InstanceDomain = database.NewNullString(req.Msg.GetSubscription().GetInstanceDomain()) + } + if p == "display_name" || p == "*" { + valid = true opts.DisplayName = database.NewNullString(req.Msg.GetSubscription().GetDisplayName()) - default: + } + if p == "salesforce.subscription_id" || p == "*" { + valid = true + opts.SalesforceSubscriptionID = + database.NewNullString(req.Msg.GetSubscription().GetSalesforce().GetSubscriptionId()) + } + + if !valid { return nil, connect.NewError(connect.CodeInvalidArgument, errors.Newf("unknown field path: %s", p)) } } @@ -442,11 +568,208 @@ func (s *handlerV1) UpdateEnterpriseSubscription(ctx context.Context, req *conne ), nil } +func (s *handlerV1) ArchiveEnterpriseSubscription(ctx context.Context, req *connect.Request[subscriptionsv1.ArchiveEnterpriseSubscriptionRequest]) (*connect.Response[subscriptionsv1.ArchiveEnterpriseSubscriptionResponse], error) { + logger := trace.Logger(ctx, s.logger) + + // 🚨 SECURITY: Require appropriate M2M scope. + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, scopes.ActionWrite) + clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) + if err != nil { + return nil, err + } + logger = logger.With(clientAttrs...) + + subscriptionID := req.Msg.GetSubscriptionId() + if subscriptionID == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("subscription_id is required")) + } + + if _, err := s.store.GetEnterpriseSubscription(ctx, subscriptionID); err != nil { + if errors.Is(err, subscriptions.ErrSubscriptionNotFound) { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to find subscription") + } + + archivedAt := s.store.Now() + + // First, revoke all licenses associated with this subscription + licenses, err := s.store.ListEnterpriseSubscriptionLicenses(ctx, subscriptions.ListLicensesOpts{ + SubscriptionID: subscriptionID, + }) + if err != nil { + return nil, connectutil.InternalError(ctx, logger, err, "failed to list licenses for subscription") + } + revokedLicenses := make([]string, 0, len(licenses)) + for _, lc := range licenses { + // Already revoked - nothing to do + if lc.RevokedAt != nil { + continue + } + + licenseRevokeReason := "Subscription archival" + if reason := req.Msg.GetReason(); reason != "" { + licenseRevokeReason = fmt.Sprintf("Subscription archival: %s", reason) + } + _, err := s.store.RevokeEnterpriseSubscriptionLicense(ctx, lc.ID, subscriptions.RevokeLicenseOpts{ + Message: licenseRevokeReason, + Time: &archivedAt, + }) + if err != nil { + // Audit-log the licenses we did manage to revoke + logger.Scoped("audit").Info("ArchiveEnterpriseSubscription", + log.Strings("revokedLicenses", revokedLicenses)) + + return nil, connectutil.InternalError(ctx, logger, err, + fmt.Sprintf("failed to revoke license %q", lc.ID)) + } + + revokedLicenses = append(revokedLicenses, lc.ID) + } + + // Then, archive the parent subscription + createdSub, err := s.store.UpsertEnterpriseSubscription(ctx, subscriptionID, + subscriptions.UpsertSubscriptionOptions{ + ArchivedAt: pointers.Ptr(archivedAt), + }, + subscriptions.CreateSubscriptionConditionOptions{ + Status: subscriptionsv1.EnterpriseSubscriptionCondition_STATUS_ARCHIVED, + TransitionTime: archivedAt, + Message: req.Msg.GetReason(), + }) + if err != nil { + if errors.Is(err, subscriptions.ErrInvalidArgument) { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to create subscription") + } + + protoSub := convertSubscriptionToProto(createdSub) + logger.Scoped("audit").Info("ArchiveEnterpriseSubscription", + log.String("archivedSubscription", protoSub.GetId()), + log.Strings("revokedLicenses", revokedLicenses)) + return connect.NewResponse(&subscriptionsv1.ArchiveEnterpriseSubscriptionResponse{}), nil +} + +func (s *handlerV1) CreateEnterpriseSubscriptionLicense(ctx context.Context, req *connect.Request[subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest]) (*connect.Response[subscriptionsv1.CreateEnterpriseSubscriptionLicenseResponse], error) { + logger := trace.Logger(ctx, s.logger) + + // 🚨 SECURITY: Require appropriate M2M scope. + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, scopes.ActionWrite) + clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) + if err != nil { + return nil, err + } + logger = logger.With(clientAttrs...) + + create := req.Msg.GetLicense() + if create.GetId() != "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("license.id cannot be set")) + } + subscriptionID := create.GetSubscriptionId() + if subscriptionID == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("license.subscription_id is required")) + } + sub, err := s.store.GetEnterpriseSubscription(ctx, subscriptionID) + if err != nil { + if errors.Is(err, subscriptions.ErrSubscriptionNotFound) { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to find subscription") + } + if sub.ArchivedAt != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, + errors.New("target subscription is archived")) + } + + createdAt := s.store.Now() + + var createdLicense *subscriptions.LicenseWithConditions + switch data := create.License.(type) { + case *subscriptionsv1.EnterpriseSubscriptionLicense_Key: + licenseKey, err := convertLicenseKeyToLicenseKeyData( + createdAt, + &sub.Subscription, + data.Key, + s.store.GetRequiredEnterpriseSubscriptionLicenseKeyTags(), + s.store.SignEnterpriseSubscriptionLicenseKey) + if err != nil { + var connectErr *connect.Error + if errors.As(err, &connectErr) { + return nil, err + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to initialize license key from inputs") + } + createdLicense, err = s.store.CreateEnterpriseSubscriptionLicenseKey(ctx, subscriptionID, + licenseKey, + subscriptions.CreateLicenseOpts{ + Message: req.Msg.GetMessage(), + Time: &createdAt, + ExpireTime: utctime.FromTime(licenseKey.Info.ExpiresAt), + }) + if err != nil { + return nil, connectutil.InternalError(ctx, logger, err, "failed to create license key") + } + + default: + return nil, connect.NewError(connect.CodeInvalidArgument, errors.Newf("unsupported licnese type %T", data)) + } + + proto, err := convertLicenseToProto(createdLicense) + if err != nil { + return nil, connectutil.InternalError(ctx, logger, + errors.Wrap(err, createdLicense.ID), + "failed to parse license") + } + logger.Scoped("audit").Info("CreateEnterpriseSubscriptionLicense", + log.String("subscription", subscriptionID), + log.String("createdLicense", proto.GetId())) + return connect.NewResponse(&subscriptionsv1.CreateEnterpriseSubscriptionLicenseResponse{ + License: proto, + }), nil +} + +func (s *handlerV1) RevokeEnterpriseSubscriptionLicense(ctx context.Context, req *connect.Request[subscriptionsv1.RevokeEnterpriseSubscriptionLicenseRequest]) (*connect.Response[subscriptionsv1.RevokeEnterpriseSubscriptionLicenseResponse], error) { + logger := trace.Logger(ctx, s.logger) + + // 🚨 SECURITY: Require appropriate M2M scope. + requiredScope := samsm2m.EnterprisePortalScope("subscription", scopes.ActionWrite) + clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) + if err != nil { + return nil, err + } + logger = logger.With(clientAttrs...) + + licenseID := req.Msg.LicenseId + if licenseID == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("license_id is required")) + } + + license, err := s.store.RevokeEnterpriseSubscriptionLicense(ctx, licenseID, subscriptions.RevokeLicenseOpts{ + Message: req.Msg.GetReason(), + Time: pointers.Ptr(s.store.Now()), + }) + if err != nil { + if errors.Is(err, subscriptions.ErrSubscriptionLicenseNotFound) { + return nil, connect.NewError(connect.CodeNotFound, err) + } + return nil, connectutil.InternalError(ctx, logger, err, "failed to revoked license") + } + + logger.Scoped("audit").Info("RevokeEnterpriseSubscriptionLicense", + log.String("subscription", license.SubscriptionID), + log.String("revokedLicense", license.ID)) + return connect.NewResponse(&subscriptionsv1.RevokeEnterpriseSubscriptionLicenseResponse{}), nil +} + func (s *handlerV1) UpdateEnterpriseSubscriptionMembership(ctx context.Context, req *connect.Request[subscriptionsv1.UpdateEnterpriseSubscriptionMembershipRequest]) (*connect.Response[subscriptionsv1.UpdateEnterpriseSubscriptionMembershipResponse], error) { logger := trace.Logger(ctx, s.logger) // 🚨 SECURITY: Require appropriate M2M scope. - requiredScope := samsm2m.EnterprisePortalScope("permission.subscription", scopes.ActionWrite) + requiredScope := samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscriptionPermission, scopes.ActionWrite) clientAttrs, err := samsm2m.RequireScope(ctx, logger, s.store, requiredScope, req) if err != nil { return nil, err @@ -477,13 +800,11 @@ func (s *handlerV1) UpdateEnterpriseSubscriptionMembership(ctx context.Context, if subscriptionID != "" { // Double check that the subscription ID is valid. - subscriptionAttrs, err := s.store.ListEnterpriseSubscriptions(ctx, subscriptions.ListEnterpriseSubscriptionsOptions{ - IDs: []string{subscriptionID}, - }) - if err != nil { - return nil, connectutil.InternalError(ctx, logger, err, "get dotcom enterprise subscription") - } else if len(subscriptionAttrs) != 1 { - return nil, connect.NewError(connect.CodeNotFound, errors.New("subscription not found")) + if _, err := s.store.GetEnterpriseSubscription(ctx, subscriptionID); err != nil { + if errors.Is(err, subscriptions.ErrSubscriptionNotFound) { + return nil, connect.NewError(connect.CodeNotFound, errors.New("subscription not found")) + } + return nil, connectutil.InternalError(ctx, logger, err, "get enterprise subscription") } } else if instanceDomain != "" { // Validate and normalize the domain @@ -495,7 +816,7 @@ func (s *handlerV1) UpdateEnterpriseSubscriptionMembership(ctx context.Context, ctx, subscriptions.ListEnterpriseSubscriptionsOptions{ InstanceDomains: []string{instanceDomain}, - PageSize: 1, + PageSize: 1, // instanceDomain should be globally unique }, ) if err != nil { diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go b/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go index 1c09368b65ce7..b58d9de8e13f8 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/v1_store.go @@ -2,12 +2,20 @@ package subscriptionsservice import ( "context" + "strings" + + "github.com/google/uuid" + "golang.org/x/crypto/ssh" sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go" clientsv1 "github.com/sourcegraph/sourcegraph-accounts-sdk-go/clients/v1" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime" + "github.com/sourcegraph/sourcegraph/internal/license" + subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/iam" ) @@ -15,17 +23,36 @@ import ( // is meant to abstract away and limit the exposure of the underlying data layer // to the handler through a thin-wrapper. type StoreV1 interface { + // Now provides the current time. It should always be used instead of + // utctime.Now() or time.Now() for ease of mocking in tests. + Now() utctime.Time + + // GenerateSubscriptionID generates a new subscription ID for subscription + // creation. + GenerateSubscriptionID() (string, error) // UpsertEnterpriseSubscription upserts a enterprise subscription record based // on the given options. - UpsertEnterpriseSubscription(ctx context.Context, subscriptionID string, opts subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) + UpsertEnterpriseSubscription(ctx context.Context, subscriptionID string, opts subscriptions.UpsertSubscriptionOptions, conditions ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) // ListEnterpriseSubscriptions returns a list of enterprise subscriptions based // on the given options. ListEnterpriseSubscriptions(ctx context.Context, opts subscriptions.ListEnterpriseSubscriptionsOptions) ([]*subscriptions.SubscriptionWithConditions, error) + // GetEnterpriseSubscriptions returns a specific enterprise subscription. + // + // Returns subscriptions.ErrSubscriptionNotFound if the subscription does + // not exist. + GetEnterpriseSubscription(ctx context.Context, subscriptionID string) (*subscriptions.SubscriptionWithConditions, error) + // ListDotcomEnterpriseSubscriptionLicenses returns a list of enterprise // subscription license attributes with the given filters. It silently ignores // any non-matching filters. The caller should check the length of the returned // slice to ensure all requested licenses were found. ListEnterpriseSubscriptionLicenses(ctx context.Context, opts subscriptions.ListLicensesOpts) ([]*subscriptions.LicenseWithConditions, error) + // RevokeEnterpriseSubscriptionLicense premanently revokes a license. + RevokeEnterpriseSubscriptionLicense(ctx context.Context, licenseID string, opts subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) + + // Interfaces specific to 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY', grouped + // to clarify their purpose for future license key types. + licenseKeysStore // IntrospectSAMSToken takes a SAMS access token and returns relevant metadata. // @@ -48,39 +75,124 @@ type StoreV1 interface { IAMCheck(ctx context.Context, opts iam.CheckOptions) (allowed bool, _ error) } +// licenseKeysStore groups mechanisms specific to the license type +// 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' +type licenseKeysStore interface { + // GetRequiredEnterpriseSubscriptionLicenseKeyTags returns the license tags + // that must be included on all generated license keys. + GetRequiredEnterpriseSubscriptionLicenseKeyTags() []string + // SignEnterpriseSubscriptionLicenseKey signs a new license key for + // creation of 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' licenses. + // + // Returns errStoreUnimplemented if key signing is not configured. + SignEnterpriseSubscriptionLicenseKey(license.Info) (string, error) + // CreateLicense creates a new classic offline license for the given subscription. + CreateEnterpriseSubscriptionLicenseKey(ctx context.Context, subscriptionID string, license *subscriptions.DataLicenseKey, opts subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) +} + type storeV1 struct { db *database.DB SAMSClient *sams.ClientV1 IAMClient *iam.ClientV1 + // LicenseKeySigner may be nil if not configured for key signing. + LicenseKeySigner ssh.Signer + LicenseKeyRequiredTags []string } type NewStoreV1Options struct { DB *database.DB SAMSClient *sams.ClientV1 IAMClient *iam.ClientV1 + + LicenseKeySigner ssh.Signer + LicenseKeyRequiredTags []string } +var errStoreUnimplemented = errors.New("unimplemented") + // NewStoreV1 returns a new StoreV1 using the given resource handles. func NewStoreV1(opts NewStoreV1Options) StoreV1 { return &storeV1{ db: opts.DB, SAMSClient: opts.SAMSClient, IAMClient: opts.IAMClient, + + LicenseKeySigner: opts.LicenseKeySigner, + LicenseKeyRequiredTags: opts.LicenseKeyRequiredTags, + } +} + +func (s *storeV1) Now() utctime.Time { return utctime.Now() } + +func (s *storeV1) GenerateSubscriptionID() (string, error) { + id, err := uuid.NewRandom() + if err != nil { + return "", errors.Wrap(err, "uuid") } + return id.String(), nil } -func (s *storeV1) UpsertEnterpriseSubscription(ctx context.Context, subscriptionID string, opts subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) { - return s.db.Subscriptions().Upsert(ctx, subscriptionID, opts) +func (s *storeV1) UpsertEnterpriseSubscription(ctx context.Context, subscriptionID string, opts subscriptions.UpsertSubscriptionOptions, conditions ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { + return s.db.Subscriptions().Upsert( + ctx, + strings.TrimPrefix(subscriptionID, subscriptionsv1.EnterpriseSubscriptionIDPrefix), + opts, + conditions..., + ) } func (s *storeV1) ListEnterpriseSubscriptions(ctx context.Context, opts subscriptions.ListEnterpriseSubscriptionsOptions) ([]*subscriptions.SubscriptionWithConditions, error) { + for idx := range opts.IDs { + opts.IDs[idx] = strings.TrimPrefix(opts.IDs[idx], subscriptionsv1.EnterpriseSubscriptionIDPrefix) + } return s.db.Subscriptions().List(ctx, opts) } +func (s *storeV1) GetEnterpriseSubscription(ctx context.Context, subscriptionID string) (*subscriptions.SubscriptionWithConditions, error) { + return s.db.Subscriptions().Get(ctx, + strings.TrimPrefix(subscriptionID, subscriptionsv1.EnterpriseSubscriptionIDPrefix)) +} + func (s *storeV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, opts subscriptions.ListLicensesOpts) ([]*subscriptions.LicenseWithConditions, error) { + opts.SubscriptionID = strings.TrimPrefix(opts.SubscriptionID, subscriptionsv1.EnterpriseSubscriptionIDPrefix) return s.db.Subscriptions().Licenses().List(ctx, opts) } +func (s *storeV1) GetRequiredEnterpriseSubscriptionLicenseKeyTags() []string { + return s.LicenseKeyRequiredTags +} + +func (s *storeV1) SignEnterpriseSubscriptionLicenseKey(info license.Info) (string, error) { + if s.LicenseKeySigner == nil { + return "", errStoreUnimplemented + } + signedKey, _, err := license.GenerateSignedKey(info, s.LicenseKeySigner) + if err != nil { + return "", errors.Wrap(err, "generating signed key") + } + return signedKey, nil +} + +func (s *storeV1) CreateEnterpriseSubscriptionLicenseKey(ctx context.Context, subscriptionID string, license *subscriptions.DataLicenseKey, opts subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + if opts.ImportLicenseID != "" { + return nil, errors.New("import license ID not allowed via API") + } + return s.db.Subscriptions().Licenses().CreateLicenseKey( + ctx, + strings.TrimPrefix(subscriptionID, subscriptionsv1.EnterpriseSubscriptionIDPrefix), + license, + opts, + ) +} + +func (s *storeV1) RevokeEnterpriseSubscriptionLicense(ctx context.Context, licenseID string, opts subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + return s.db.Subscriptions().Licenses().Revoke( + ctx, + strings.TrimPrefix(licenseID, subscriptionsv1.EnterpriseSubscriptionLicenseIDPrefix), + opts, + ) +} + func (s *storeV1) IntrospectSAMSToken(ctx context.Context, token string) (*sams.IntrospectTokenResponse, error) { return s.SAMSClient.Tokens().IntrospectToken(ctx, token) } diff --git a/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go b/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go index 341daabee0464..7ac68f6b3ea29 100644 --- a/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go +++ b/cmd/enterprise-portal/internal/subscriptionsservice/v1_test.go @@ -3,25 +3,35 @@ package subscriptionsservice import ( "context" "database/sql" + "encoding/json" "fmt" "slices" + "sync/atomic" "testing" + "time" "connectrpc.com/connect" mockrequire "github.com/derision-test/go-mockgen/v2/testutil/require" + "github.com/google/uuid" "github.com/hexops/autogold/v2" + "github.com/hexops/valast" "github.com/sourcegraph/log/logtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/fieldmaskpb" + "google.golang.org/protobuf/types/known/timestamppb" sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go" "github.com/sourcegraph/sourcegraph-accounts-sdk-go/scopes" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/subscriptions" + "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/database/utctime" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/samsm2m" + "github.com/sourcegraph/sourcegraph/internal/license" subscriptionsv1 "github.com/sourcegraph/sourcegraph/lib/enterpriseportal/subscriptions/v1" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/iam" + "github.com/sourcegraph/sourcegraph/lib/pointers" ) type testHandlerV1 struct { @@ -29,22 +39,44 @@ type testHandlerV1 struct { mockStore *MockStoreV1 } -func newTestHandlerV1() *testHandlerV1 { +func newPredictableGenerator(ns string) func() (string, error) { + var seq atomic.Int32 + return func() (string, error) { + return fmt.Sprintf("%s-%d", ns, seq.Add(1)), nil + } +} + +func newMockTime() time.Time { + return time.Date(2024, 1, 1, 1, 1, 0, 0, time.UTC) +} + +func newTestHandlerV1(t *testing.T, tokenScopes ...scopes.Scope) *testHandlerV1 { mockStore := NewMockStoreV1() mockStore.IntrospectSAMSTokenFunc.SetDefaultReturn( &sams.IntrospectTokenResponse{ Active: true, - Scopes: scopes.Scopes{ - samsm2m.EnterprisePortalScope("subscription", scopes.ActionRead), - samsm2m.EnterprisePortalScope("subscription", scopes.ActionWrite), - samsm2m.EnterprisePortalScope("permission.subscription", scopes.ActionWrite), - }, + Scopes: tokenScopes, }, nil, ) + + mockStore.GenerateSubscriptionIDFunc.SetDefaultHook( + newPredictableGenerator("uuid")) + + keySigner := newPredictableGenerator("signedkey") + mockStore.SignEnterpriseSubscriptionLicenseKeyFunc.SetDefaultHook( + func(i license.Info) (string, error) { return keySigner() }) + + // Stable time generator that increments by 1 second on each call + var timeSeq atomic.Int32 + mockStore.NowFunc.SetDefaultHook(func() utctime.Time { + return utctime.FromTime(newMockTime(). + Add(time.Duration(timeSeq.Add(1)) * time.Second)) + }) + return &testHandlerV1{ handlerV1: &handlerV1{ - logger: logtest.NoOp(nil), + logger: logtest.Scoped(t), store: mockStore, }, mockStore: mockStore, @@ -169,7 +201,12 @@ func TestHandlerV1_ListEnterpriseSubscriptions(t *testing.T) { req := connect.NewRequest(tc.list) req.Header().Add("Authorization", "Bearer foolmeifyoucan") - h := newTestHandlerV1() + h := newTestHandlerV1(t, + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionRead, + ), + ) h.mockStore.IAMListObjectsFunc.SetDefaultHook(func(_ context.Context, opts iam.ListObjectsOptions) ([]string, error) { return tc.iamObjectsHook(opts) }) @@ -198,15 +235,194 @@ func TestHandlerV1_ListEnterpriseSubscriptions(t *testing.T) { } } +func TestHandlerV1_CreateEnterpriseSubscription(t *testing.T) { + ctx := context.Background() + + for _, tc := range []struct { + name string + tokenScopes scopes.Scopes + create *subscriptionsv1.CreateEnterpriseSubscriptionRequest + wantError autogold.Value + wantUpsertOpts autogold.Value + }{ + { + name: "no parameters", + create: &subscriptionsv1.CreateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{}, + }, + wantError: autogold.Expect("invalid_argument: display_name is required"), + }, + { + name: "custom subscription ID", + create: &subscriptionsv1.CreateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + Id: "not-allowed", + DisplayName: t.Name(), + }, + }, + wantError: autogold.Expect("invalid_argument: subscription_id can not be set"), + }, + { + name: "insufficient scopes", + tokenScopes: scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionRead, + ), + }, + create: &subscriptionsv1.CreateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + Id: "not-allowed", + DisplayName: t.Name(), + }, + }, + wantError: autogold.Expect("permission_denied: insufficient scope"), + }, + { + name: "with required params only", + create: &subscriptionsv1.CreateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + DisplayName: t.Name(), + }, + }, + wantUpsertOpts: autogold.Expect(subscriptions.UpsertSubscriptionOptions{ + InstanceDomain: &sql.NullString{}, + DisplayName: &sql.NullString{ + String: "TestHandlerV1_CreateEnterpriseSubscription", + Valid: true, + }, + CreatedAt: utctime.Date(2024, + 1, + 1, + 1, + 1, + 1, + 0), + SalesforceSubscriptionID: &sql.NullString{}, + }), + }, + { + name: "with message and optional fields", + create: &subscriptionsv1.CreateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + DisplayName: t.Name(), + Salesforce: &subscriptionsv1.EnterpriseSubscriptionSalesforceMetadata{ + SubscriptionId: "sf_sub", + }, + }, + Message: "hello world", + }, + wantUpsertOpts: autogold.Expect(subscriptions.UpsertSubscriptionOptions{ + InstanceDomain: &sql.NullString{}, + DisplayName: &sql.NullString{ + String: "TestHandlerV1_CreateEnterpriseSubscription", + Valid: true, + }, + CreatedAt: utctime.Date(2024, + 1, + 1, + 1, + 1, + 1, + 0), + SalesforceSubscriptionID: &sql.NullString{ + String: "sf_sub", + Valid: true, + }, + }), + }, + } { + req := connect.NewRequest(tc.create) + req.Header().Add("Authorization", "Bearer foolmeifyoucan") + + if tc.tokenScopes == nil { + tc.tokenScopes = scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionWrite, + ), + } + } + h := newTestHandlerV1(t, tc.tokenScopes...) + h.mockStore.GetEnterpriseSubscriptionFunc.SetDefaultHook(func(_ context.Context, id string) (*subscriptions.SubscriptionWithConditions, error) { + return nil, subscriptions.ErrSubscriptionNotFound + }) + h.mockStore.UpsertEnterpriseSubscriptionFunc.SetDefaultHook(func(_ context.Context, _ string, opts subscriptions.UpsertSubscriptionOptions, conds ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { + require.Len(t, conds, 1) // create must have condition + + // Condition must match upsert + assert.Equal(t, tc.create.GetMessage(), conds[0].Message) + assert.Equal(t, opts.CreatedAt, conds[0].TransitionTime) + assert.Equal(t, subscriptionsv1.EnterpriseSubscriptionCondition_STATUS_CREATED, + conds[0].Status) + + tc.wantUpsertOpts.Equal(t, opts) + + return &subscriptions.SubscriptionWithConditions{}, nil + }) + _, err := h.CreateEnterpriseSubscription(ctx, req) + if tc.wantError != nil { + require.Error(t, err) + tc.wantError.Equal(t, err.Error()) + } else { + require.NoError(t, err) + } + if tc.wantUpsertOpts != nil { + mockrequire.CalledOnce(t, h.mockStore.UpsertEnterpriseSubscriptionFunc) + mockrequire.CalledOnce(t, h.mockStore.GetEnterpriseSubscriptionFunc) + } else { + mockrequire.NotCalled(t, h.mockStore.UpsertEnterpriseSubscriptionFunc) + } + } +} + func TestHandlerV1_UpdateEnterpriseSubscription(t *testing.T) { ctx := context.Background() const mockSubscriptionID = "es_80ca12e2-54b4-448c-a61a-390b1a9c1224" for _, tc := range []struct { name string + tokenScopes scopes.Scopes update *subscriptionsv1.UpdateEnterpriseSubscriptionRequest wantUpdateOpts autogold.Value + wantError autogold.Value }{ + { + name: "insufficient scopes", + tokenScopes: scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionRead, + ), + }, + update: &subscriptionsv1.UpdateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + Id: mockSubscriptionID, + }, + UpdateMask: nil, + }, + wantError: autogold.Expect("permission_denied: insufficient scope"), + }, + { + name: "subscription ID is required", + update: &subscriptionsv1.UpdateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + Id: "", + }, + UpdateMask: nil, + }, + wantError: autogold.Expect("invalid_argument: subscription.id is required"), + }, + { + name: "subscription does not exist", + update: &subscriptionsv1.UpdateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + Id: uuid.NewString(), + }, + UpdateMask: nil, + }, + wantError: autogold.Expect("not_found: subscription not found"), + }, { name: "no update mask", update: &subscriptionsv1.UpdateEnterpriseSubscriptionRequest{ @@ -229,6 +445,18 @@ func TestHandlerV1_UpdateEnterpriseSubscription(t *testing.T) { }, }), }, + { + name: "unknown field mask", + update: &subscriptionsv1.UpdateEnterpriseSubscriptionRequest{ + Subscription: &subscriptionsv1.EnterpriseSubscription{ + Id: mockSubscriptionID, + InstanceDomain: "s1.sourcegraph.com", + DisplayName: "My Test Subscription", + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"asdfasdf"}}, + }, + wantError: autogold.Expect("invalid_argument: unknown field path: asdfasdf"), + }, { name: "specified field mask", update: &subscriptionsv1.UpdateEnterpriseSubscriptionRequest{ @@ -257,9 +485,10 @@ func TestHandlerV1_UpdateEnterpriseSubscription(t *testing.T) { }, // All update-able values should be set to their defaults explicitly wantUpdateOpts: autogold.Expect(subscriptions.UpsertSubscriptionOptions{ - InstanceDomain: &sql.NullString{}, - DisplayName: &sql.NullString{}, - ForceUpdate: true, + InstanceDomain: &sql.NullString{}, + DisplayName: &sql.NullString{}, + SalesforceSubscriptionID: &sql.NullString{}, + ForceUpdate: true, }), }, { @@ -279,19 +508,37 @@ func TestHandlerV1_UpdateEnterpriseSubscription(t *testing.T) { req := connect.NewRequest(tc.update) req.Header().Add("Authorization", "Bearer foolmeifyoucan") - h := newTestHandlerV1() - h.mockStore.ListEnterpriseSubscriptionsFunc.SetDefaultReturn( - []*subscriptions.SubscriptionWithConditions{ - {Subscription: subscriptions.Subscription{ - ID: "80ca12e2-54b4-448c-a61a-390b1a9c1224", - }}, - }, nil) - h.mockStore.UpsertEnterpriseSubscriptionFunc.SetDefaultHook(func(_ context.Context, _ string, opts subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) { + if tc.tokenScopes == nil { + tc.tokenScopes = scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionWrite, + ), + } + } + h := newTestHandlerV1(t, tc.tokenScopes...) + h.mockStore.GetEnterpriseSubscriptionFunc.SetDefaultHook(func(ctx context.Context, id string) (*subscriptions.SubscriptionWithConditions, error) { + if id == mockSubscriptionID { + return &subscriptions.SubscriptionWithConditions{ + Subscription: subscriptions.Subscription{ + ID: id, + }, + }, nil + } + return nil, subscriptions.ErrSubscriptionNotFound + }) + h.mockStore.UpsertEnterpriseSubscriptionFunc.SetDefaultHook(func(_ context.Context, _ string, opts subscriptions.UpsertSubscriptionOptions, conds ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { tc.wantUpdateOpts.Equal(t, opts) + assert.Len(t, conds, 0) // no conditions for standard updates return &subscriptions.SubscriptionWithConditions{}, nil }) _, err := h.UpdateEnterpriseSubscription(ctx, req) - require.NoError(t, err) + if tc.wantError != nil { + require.Error(t, err) + tc.wantError.Equal(t, err.Error()) + } else { + require.NoError(t, err) + } if tc.wantUpdateOpts != nil { mockrequire.CalledOnce(t, h.mockStore.UpsertEnterpriseSubscriptionFunc) } else { @@ -301,6 +548,418 @@ func TestHandlerV1_UpdateEnterpriseSubscription(t *testing.T) { } } +func TestHandlerV1_ArchiveEnterpriseSubscription(t *testing.T) { + ctx := context.Background() + const mockSubscriptionID = "es_80ca12e2-54b4-448c-a61a-390b1a9c1224" + const mockLicenseID = "esl_80ca12e2-54b4-448c-a61a-390b1a9c1224" + + for _, tc := range []struct { + name string + tokenScopes scopes.Scopes + archive *subscriptionsv1.ArchiveEnterpriseSubscriptionRequest + wantUpsertOpts autogold.Value + wantError autogold.Value + }{ + { + name: "insufficient scopes", + tokenScopes: scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionRead, + ), + }, + archive: &subscriptionsv1.ArchiveEnterpriseSubscriptionRequest{}, + wantError: autogold.Expect("permission_denied: insufficient scope"), + }, + { + name: "subscription ID is required", + archive: &subscriptionsv1.ArchiveEnterpriseSubscriptionRequest{}, + wantError: autogold.Expect("invalid_argument: subscription_id is required"), + }, + { + name: "subscription does not exist", + archive: &subscriptionsv1.ArchiveEnterpriseSubscriptionRequest{ + SubscriptionId: uuid.NewString(), + }, + wantError: autogold.Expect("not_found: subscription not found"), + }, + { + name: "ok with reason", + archive: &subscriptionsv1.ArchiveEnterpriseSubscriptionRequest{ + SubscriptionId: mockSubscriptionID, + Reason: t.Name(), + }, + wantUpsertOpts: autogold.Expect(subscriptions.UpsertSubscriptionOptions{ArchivedAt: valast.Ptr(utctime.Date(2024, + 1, + 1, + 1, + 1, + 1, + 0))}), + }, + } { + t.Run(tc.name, func(t *testing.T) { + req := connect.NewRequest(tc.archive) + req.Header().Add("Authorization", "Bearer foolmeifyoucan") + + if tc.tokenScopes == nil { + tc.tokenScopes = scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionWrite, + ), + } + } + h := newTestHandlerV1(t, tc.tokenScopes...) + h.mockStore.GetEnterpriseSubscriptionFunc.SetDefaultHook(func(_ context.Context, id string) (*subscriptions.SubscriptionWithConditions, error) { + if id == mockSubscriptionID { + return &subscriptions.SubscriptionWithConditions{ + Subscription: subscriptions.Subscription{ + ID: id, + }, + }, nil + } + return nil, subscriptions.ErrSubscriptionNotFound + }) + h.mockStore.ListEnterpriseSubscriptionLicensesFunc.SetDefaultHook(func(_ context.Context, opts subscriptions.ListLicensesOpts) ([]*subscriptions.LicenseWithConditions, error) { + if opts.SubscriptionID == mockSubscriptionID { + return []*subscriptions.LicenseWithConditions{{ + SubscriptionLicense: subscriptions.SubscriptionLicense{ + ID: mockLicenseID, + }, + }, { + SubscriptionLicense: subscriptions.SubscriptionLicense{ + ID: "esl_already_revoked", + RevokedAt: pointers.Ptr(utctime.Now()), + }, + }}, nil + } + return nil, errors.New("unexpected subscription ID") + }) + h.mockStore.RevokeEnterpriseSubscriptionLicenseFunc.SetDefaultHook(func(_ context.Context, l string, opts subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + assert.Equal(t, mockLicenseID, l) + assert.Contains(t, opts.Message, tc.archive.GetReason()) + require.NotNil(t, opts.Time) + return &subscriptions.LicenseWithConditions{}, nil + }) + h.mockStore.UpsertEnterpriseSubscriptionFunc.SetDefaultHook(func(_ context.Context, _ string, opts subscriptions.UpsertSubscriptionOptions, conds ...subscriptions.CreateSubscriptionConditionOptions) (*subscriptions.SubscriptionWithConditions, error) { + require.Len(t, conds, 1) // create must have condition + + // Condition must match upsert + assert.Equal(t, tc.archive.GetReason(), conds[0].Message) + require.NotNil(t, opts.ArchivedAt) + assert.Equal(t, *opts.ArchivedAt, conds[0].TransitionTime) + assert.Equal(t, subscriptionsv1.EnterpriseSubscriptionCondition_STATUS_ARCHIVED, + conds[0].Status) + + tc.wantUpsertOpts.Equal(t, opts, autogold.ExportedOnly()) + + return &subscriptions.SubscriptionWithConditions{}, nil + }) + _, err := h.ArchiveEnterpriseSubscription(ctx, req) + if tc.wantError != nil { + require.Error(t, err) + tc.wantError.Equal(t, err.Error()) + } else { + require.NoError(t, err) + } + if tc.wantUpsertOpts != nil { + mockrequire.CalledOnce(t, h.mockStore.UpsertEnterpriseSubscriptionFunc) + mockrequire.CalledOnce(t, h.mockStore.RevokeEnterpriseSubscriptionLicenseFunc) + } else { + mockrequire.NotCalled(t, h.mockStore.UpsertEnterpriseSubscriptionFunc) + } + }) + } +} + +func TestHandlerV1_CreateEnterpriseSubscriptionLicense(t *testing.T) { + ctx := context.Background() + const mockSubscriptionID = "es_80ca12e2-54b4-448c-a61a-390b1a9c1224" + const archivedSubscriptionID = "es_b7df32dd-509c-4114-a6bb-4e6d09090fba" + requiredTags := []string{"test", "dev"} + + for _, tc := range []struct { + name string + tokenScopes scopes.Scopes + create *subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest + wantKeyOpts autogold.Value + wantError autogold.Value + }{ + { + name: "insufficient scopes", + tokenScopes: scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionRead, + ), + }, + create: &subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest{}, + wantError: autogold.Expect("permission_denied: insufficient scope"), + }, + { + name: "subscription ID is required", + create: &subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest{}, + wantError: autogold.Expect("invalid_argument: license.subscription_id is required"), + }, + { + name: "subscription does not exist", + create: &subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest{ + License: &subscriptionsv1.EnterpriseSubscriptionLicense{ + SubscriptionId: uuid.NewString(), + }, + }, + wantError: autogold.Expect("not_found: subscription not found"), + }, + { + name: "license data required", + create: &subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest{ + License: &subscriptionsv1.EnterpriseSubscriptionLicense{ + SubscriptionId: mockSubscriptionID, + }, + }, + wantError: autogold.Expect("invalid_argument: unsupported licnese type "), + }, + { + name: "license key: required tags not provided", + create: &subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest{ + License: &subscriptionsv1.EnterpriseSubscriptionLicense{ + SubscriptionId: mockSubscriptionID, + License: &subscriptionsv1.EnterpriseSubscriptionLicense_Key{}, + }, + }, + wantError: autogold.Expect("invalid_argument: user_count is invalid"), + }, + { + name: "license key: expiration is required", + create: &subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest{ + License: &subscriptionsv1.EnterpriseSubscriptionLicense{ + SubscriptionId: mockSubscriptionID, + License: &subscriptionsv1.EnterpriseSubscriptionLicense_Key{ + Key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + Tags: requiredTags, + UserCount: 100, + }, + }, + }, + }, + }, + wantError: autogold.Expect("invalid_argument: expiry must be in the future"), + }, + { + name: "license key: ok with reason", + create: &subscriptionsv1.CreateEnterpriseSubscriptionLicenseRequest{ + License: &subscriptionsv1.EnterpriseSubscriptionLicense{ + SubscriptionId: mockSubscriptionID, + License: &subscriptionsv1.EnterpriseSubscriptionLicense_Key{ + Key: &subscriptionsv1.EnterpriseSubscriptionLicenseKey{ + Info: &subscriptionsv1.EnterpriseSubscriptionLicenseKey_Info{ + Tags: requiredTags, + UserCount: 100, + // time in the future relative to newMockTime + ExpireTime: timestamppb.New(newMockTime().Add(time.Hour)), + }, + }, + }, + }, + Message: t.Name(), + }, + wantKeyOpts: autogold.Expect(&subscriptions.DataLicenseKey{ + Info: license.Info{ + Tags: []string{ + "test", + "dev", + }, + UserCount: 100, + CreatedAt: time.Date(2024, + 1, + 1, + 1, + 1, + 1, + 0, + time.UTC), + ExpiresAt: time.Date(2024, + 1, + 1, + 2, + 1, + 0, + 0, + time.UTC), + }, + SignedKey: "signedkey-1", + }), + }, + } { + t.Run(tc.name, func(t *testing.T) { + req := connect.NewRequest(tc.create) + req.Header().Add("Authorization", "Bearer foolmeifyoucan") + + if tc.tokenScopes == nil { + tc.tokenScopes = scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionWrite, + ), + } + } + h := newTestHandlerV1(t, tc.tokenScopes...) + h.mockStore.GetEnterpriseSubscriptionFunc.SetDefaultHook(func(ctx context.Context, id string) (*subscriptions.SubscriptionWithConditions, error) { + if id == mockSubscriptionID { + return &subscriptions.SubscriptionWithConditions{ + Subscription: subscriptions.Subscription{ + ID: id, + }, + }, nil + } + if id == archivedSubscriptionID { + return &subscriptions.SubscriptionWithConditions{ + Subscription: subscriptions.Subscription{ + ID: id, + ArchivedAt: pointers.Ptr(utctime.FromTime( + // Archived in the past relative to newMockTime + newMockTime().Add(-1 * time.Hour)), + ), + }, + }, nil + } + return nil, subscriptions.ErrSubscriptionNotFound + }) + h.mockStore.GetRequiredEnterpriseSubscriptionLicenseKeyTagsFunc.SetDefaultReturn( + requiredTags, + ) + h.mockStore.CreateEnterpriseSubscriptionLicenseKeyFunc.SetDefaultHook(func(_ context.Context, subscription string, key *subscriptions.DataLicenseKey, opts subscriptions.CreateLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + assert.Empty(t, opts.ImportLicenseID) + // Condition must match upsert + assert.Equal(t, tc.create.GetMessage(), opts.Message) + require.NotNil(t, opts.Time) + assert.Equal(t, key.Info.CreatedAt, opts.Time.AsTime()) + require.NotZero(t, opts.ExpireTime) + assert.Equal(t, key.Info.ExpiresAt, opts.ExpireTime.AsTime()) + + tc.wantKeyOpts.Equal(t, key) + + return &subscriptions.LicenseWithConditions{ + SubscriptionLicense: subscriptions.SubscriptionLicense{ + LicenseType: "ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY", + LicenseData: json.RawMessage("{}"), + }, + }, nil + }) + _, err := h.CreateEnterpriseSubscriptionLicense(ctx, req) + if tc.wantError != nil { + require.Error(t, err) + tc.wantError.Equal(t, err.Error()) + } else { + require.NoError(t, err) + } + if tc.wantKeyOpts != nil { + mockrequire.CalledOnce(t, h.mockStore.CreateEnterpriseSubscriptionLicenseKeyFunc) + } else { + mockrequire.NotCalled(t, h.mockStore.CreateEnterpriseSubscriptionLicenseKeyFunc) + } + }) + } +} + +func TestHandlerV1_RevokeEnterpriseSubscriptionLicense(t *testing.T) { + ctx := context.Background() + const mockLicenseID = "es_80ca12e2-54b4-448c-a61a-390b1a9c1224" + + for _, tc := range []struct { + name string + tokenScopes scopes.Scopes + revoke *subscriptionsv1.RevokeEnterpriseSubscriptionLicenseRequest + wantRevokeOpts autogold.Value + wantError autogold.Value + }{ + { + name: "insufficient scopes", + tokenScopes: scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionRead, + ), + }, + revoke: &subscriptionsv1.RevokeEnterpriseSubscriptionLicenseRequest{}, + wantError: autogold.Expect("permission_denied: insufficient scope"), + }, + { + name: "license ID is required", + revoke: &subscriptionsv1.RevokeEnterpriseSubscriptionLicenseRequest{}, + wantError: autogold.Expect("invalid_argument: license_id is required"), + }, + { + name: "license does not exist", + revoke: &subscriptionsv1.RevokeEnterpriseSubscriptionLicenseRequest{ + LicenseId: uuid.NewString(), + }, + wantError: autogold.Expect("not_found: subscription license not found"), + }, + { + name: "revoke ok with reason", + revoke: &subscriptionsv1.RevokeEnterpriseSubscriptionLicenseRequest{ + LicenseId: mockLicenseID, + Reason: t.Name(), + }, + wantRevokeOpts: autogold.Expect(subscriptions.RevokeLicenseOpts{ + Message: "TestHandlerV1_RevokeEnterpriseSubscriptionLicense", + Time: valast.Ptr(utctime.Date(2024, + 1, + 1, + 1, + 1, + 1, + 0)), + }), + }, + } { + t.Run(tc.name, func(t *testing.T) { + req := connect.NewRequest(tc.revoke) + req.Header().Add("Authorization", "Bearer foolmeifyoucan") + + if tc.tokenScopes == nil { + tc.tokenScopes = scopes.Scopes{ + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscription, + scopes.ActionWrite, + ), + } + } + h := newTestHandlerV1(t, tc.tokenScopes...) + h.mockStore.RevokeEnterpriseSubscriptionLicenseFunc.SetDefaultHook(func(ctx context.Context, licenseID string, opts subscriptions.RevokeLicenseOpts) (*subscriptions.LicenseWithConditions, error) { + if licenseID != mockLicenseID { + return nil, subscriptions.ErrSubscriptionLicenseNotFound + } + + // Condition must match upsert + assert.Equal(t, tc.revoke.GetReason(), opts.Message) + + tc.wantRevokeOpts.Equal(t, opts) + + return &subscriptions.LicenseWithConditions{ + SubscriptionLicense: subscriptions.SubscriptionLicense{ + LicenseType: "ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY", + LicenseData: json.RawMessage("{}"), + }, + }, nil + }) + _, err := h.RevokeEnterpriseSubscriptionLicense(ctx, req) + if tc.wantError != nil { + require.Error(t, err) + tc.wantError.Equal(t, err.Error()) + } else { + require.NoError(t, err) + } + if tc.wantRevokeOpts != nil { + mockrequire.CalledOnce(t, h.mockStore.RevokeEnterpriseSubscriptionLicenseFunc) + } + }) + } +} + func TestHandlerV1_UpdateEnterpriseSubscriptionMembership(t *testing.T) { const ( subscriptionID = "80ca12e2-54b4-448c-a61a-390b1a9c1224" @@ -423,11 +1082,16 @@ func TestHandlerV1_UpdateEnterpriseSubscriptionMembership(t *testing.T) { req := connect.NewRequest(tc.req) req.Header().Add("Authorization", "Bearer foolmeifyoucan") - h := newTestHandlerV1() + h := newTestHandlerV1(t, + samsm2m.EnterprisePortalScope( + scopes.PermissionEnterprisePortalSubscriptionPermission, + scopes.ActionWrite, + ), + ) h.mockStore.ListEnterpriseSubscriptionsFunc.SetDefaultHook( func(_ context.Context, opts subscriptions.ListEnterpriseSubscriptionsOptions) ([]*subscriptions.SubscriptionWithConditions, error) { - if slices.Contains(opts.IDs, subscriptionID) || - slices.Contains(opts.InstanceDomains, instanceDomain) { + // List should only be called when updating via instance domain + if slices.Contains(opts.InstanceDomains, instanceDomain) { return []*subscriptions.SubscriptionWithConditions{ {Subscription: subscriptions.Subscription{ID: subscriptionID}}, }, nil @@ -435,6 +1099,18 @@ func TestHandlerV1_UpdateEnterpriseSubscriptionMembership(t *testing.T) { return nil, nil }, ) + h.mockStore.GetEnterpriseSubscriptionFunc.SetDefaultHook( + func(ctx context.Context, id string) (*subscriptions.SubscriptionWithConditions, error) { + if id == subscriptionID { + return &subscriptions.SubscriptionWithConditions{ + Subscription: subscriptions.Subscription{ + ID: subscriptionID, + }, + }, nil + } + return nil, subscriptions.ErrSubscriptionNotFound + }, + ) h.mockStore.IAMCheckFunc.SetDefaultHook(func(_ context.Context, opts iam.CheckOptions) (bool, error) { if tc.iamCheckFunc != nil { return tc.iamCheckFunc(opts) diff --git a/cmd/enterprise-portal/service/BUILD.bazel b/cmd/enterprise-portal/service/BUILD.bazel index 3f20548b5e228..42dd8cf5b88be 100644 --- a/cmd/enterprise-portal/service/BUILD.bazel +++ b/cmd/enterprise-portal/service/BUILD.bazel @@ -25,6 +25,7 @@ go_library( "//internal/codygateway/codygatewayevents", "//internal/debugserver", "//internal/httpserver", + "//internal/license", "//internal/redispool", "//internal/trace/policy", "//internal/version", @@ -46,6 +47,7 @@ go_library( "@com_github_sourcegraph_sourcegraph_accounts_sdk_go//scopes", "@io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp//:otelhttp", "@io_opentelemetry_go_otel//:otel", + "@org_golang_x_crypto//ssh", "@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 ab7f21608ddb5..daf25ad899f68 100644 --- a/cmd/enterprise-portal/service/config.go +++ b/cmd/enterprise-portal/service/config.go @@ -1,11 +1,17 @@ package service import ( + "fmt" + "strings" "time" + "golang.org/x/crypto/ssh" + sams "github.com/sourcegraph/sourcegraph-accounts-sdk-go" "github.com/sourcegraph/sourcegraph/cmd/enterprise-portal/internal/routines/licenseexpiration" "github.com/sourcegraph/sourcegraph/internal/codygateway/codygatewayevents" + "github.com/sourcegraph/sourcegraph/internal/license" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/cloudsql" "github.com/sourcegraph/sourcegraph/lib/managedservicesplatform/runtime" "github.com/sourcegraph/sourcegraph/lib/pointers" @@ -28,6 +34,15 @@ type Config struct { SAMS SAMSConfig + // Configuration specific to 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' + LicenseKeys struct { + // Signer is the private key used to generate license keys. + Signer ssh.Signer + // RequiredTags are the tags required on all licenses created in this + // Enterprise Portal instance. + RequiredTags []string + } + LicenseExpirationChecker licenseexpiration.Config } @@ -71,6 +86,33 @@ func (c *Config) Load(env *runtime.Env) { } } + c.LicenseKeys.Signer = func() ssh.Signer { + // We use a unconventional env name here to align with existing usages + // of this key, for convenience. + privateKey := env.GetOptional("SOURCEGRAPH_LICENSE_GENERATION_KEY", + fmt.Sprintf("The PEM-encoded form of the private key used to sign product license keys (%s)", + license.GenerationPrivateKeyURL)) + if privateKey == nil { + // Not having this just disables the generation of new licenses, it + // does not block startup. + return nil + } + signer, err := ssh.ParsePrivateKey([]byte(*privateKey)) + if err != nil { + env.AddError(errors.Wrap(err, + "Failed to parse private key in SOURCEGRAPH_LICENSE_GENERATION_KEY env var")) + } + return signer + }() + c.LicenseKeys.RequiredTags = func() []string { + tags := env.GetOptional("LICENSE_KEY_REQUIRED_TAGS", + "Comma-delimited list of tags required on all license keys generated on this Enterprise Portal instance") + if tags == nil { + return nil + } + return strings.Split(*tags, ",") + }() + c.LicenseExpirationChecker.Interval = env.GetOptionalInterval( "LICENSE_EXPIRATION_CHECKER_INTERVAL", "Interval at which to run license expiration checks. If not set, checks are not run.") diff --git a/cmd/enterprise-portal/service/service.go b/cmd/enterprise-portal/service/service.go index d52902cfc370a..0cba90f0340aa 100644 --- a/cmd/enterprise-portal/service/service.go +++ b/cmd/enterprise-portal/service/service.go @@ -120,9 +120,11 @@ func (Service) Initialize(ctx context.Context, logger log.Logger, contract runti httpServer, subscriptionsservice.NewStoreV1( subscriptionsservice.NewStoreV1Options{ - DB: dbHandle, - SAMSClient: samsClient, - IAMClient: iamClient, + DB: dbHandle, + SAMSClient: samsClient, + IAMClient: iamClient, + LicenseKeySigner: config.LicenseKeys.Signer, + LicenseKeyRequiredTags: config.LicenseKeys.RequiredTags, }, ), connect.WithInterceptors(otelConnctInterceptor), diff --git a/deps.bzl b/deps.bzl index 9711b7698a597..43af2a7f34acd 100644 --- a/deps.bzl +++ b/deps.bzl @@ -3430,8 +3430,9 @@ def go_dependencies(): name = "com_github_hexops_valast", build_file_proto_mode = "disable_global", importpath = "github.com/hexops/valast", - sum = "h1:rETyycw+/L2ZVJHHNxEBgh8KUn+87WugH9MxcEv9PGs=", - version = "v1.4.4", + replace = "github.com/bobheadxi/valast", + sum = "h1:cK/ixaJaSyXAKglOrY2CLE0pM9C+m2LbijsVW5k675E=", + version = "v0.0.0-20240724215614-eb5cb82e0c6f", ) go_repository( name = "com_github_hhatto_gocloc", diff --git a/go.mod b/go.mod index 3bb31c533d068..1e1d4427f17b4 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,8 @@ replace ( github.com/gomodule/redigo => github.com/gomodule/redigo v1.8.9 // Pending: Renamed to github.com/google/gnostic. Transitive deps still use the old name (kubernetes/kubernetes). github.com/googleapis/gnostic => github.com/googleapis/gnostic v0.5.5 + // Pending: https://github.com/hexops/valast/pull/27 + github.com/hexops/valast => github.com/bobheadxi/valast v0.0.0-20240724215614-eb5cb82e0c6f // Pending: https://github.com/openfga/openfga/pull/1688 github.com/openfga/openfga => github.com/sourcegraph/openfga v0.0.0-20240614204729-de6b563022de // We need to wait for https://github.com/prometheus/alertmanager to cut a diff --git a/go.sum b/go.sum index 682aa17cc4f6d..ec814e38a441a 100644 --- a/go.sum +++ b/go.sum @@ -910,6 +910,8 @@ github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQ github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bobheadxi/valast v0.0.0-20240724215614-eb5cb82e0c6f h1:cK/ixaJaSyXAKglOrY2CLE0pM9C+m2LbijsVW5k675E= +github.com/bobheadxi/valast v0.0.0-20240724215614-eb5cb82e0c6f/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -1652,9 +1654,6 @@ github.com/hexops/autogold/v2 v2.2.1 h1:JPUXuZQGkcQMv7eeDXuNMovjfoRYaa0yVcm+F3vo github.com/hexops/autogold/v2 v2.2.1/go.mod h1:IJwxtUfj1BGLm0YsR/k+dIxYi6xbeLjqGke2bzcOTMI= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/hexops/valast v1.4.3/go.mod h1:Iqx2kLj3Jn47wuXpj3wX40xn6F93QNFBHuiKBerkTGA= -github.com/hexops/valast v1.4.4 h1:rETyycw+/L2ZVJHHNxEBgh8KUn+87WugH9MxcEv9PGs= -github.com/hexops/valast v1.4.4/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4= github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/honeycombio/libhoney-go v1.15.8 h1:TECEltZ48K6J4NG1JVYqmi0vCJNnHYooFor83fgKesA= github.com/honeycombio/libhoney-go v1.15.8/go.mod h1:+tnL2etFnJmVx30yqmoUkVyQjp7uRJw0a2QGu48lSyY= diff --git a/internal/license/generate-license.go b/internal/license/generate-license.go index 1243dd68db621..a2a11a59dd09a 100644 --- a/internal/license/generate-license.go +++ b/internal/license/generate-license.go @@ -33,7 +33,7 @@ import ( ) var ( - privateKeyFile = flag.String("private-key", "", "file containing private key to sign license") + privateKeyFile = flag.String("private-key", "", fmt.Sprintf("file containing private key to sign license (e.g. the production key at %s)", license.GenerationPrivateKeyURL)) tags = flag.String("tags", "", "comma-separated string tags to include in this license (e.g., \"starter,dev\")") users = flag.Uint("users", 0, "maximum number of users allowed by this license (0 = no limit)") expires = flag.Duration("expires", 0, "time until license expires (0 = no expiration)") diff --git a/internal/license/license.go b/internal/license/license.go index 518b25367133d..a4fa7cd4cd809 100644 --- a/internal/license/license.go +++ b/internal/license/license.go @@ -21,6 +21,13 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) +// GenerationPrivateKeyURL is the URL where Sourcegraph staff can find the private key for +// generating licenses. +// +// NOTE: If you change this, use text search to replace other instances of it (in source code +// comments). +const GenerationPrivateKeyURL = "https://team-sourcegraph.1password.com/vaults/dnrhbauihkhjs5ag6vszsme45a/allitems/zkdx6gpw4uqejs3flzj7ef5j4i" + // Info contains information about a license key. In the signed license key that Sourcegraph // provides to customers, this value is signed but not encrypted. This value is not secret, and // anyone with a license key can view (but not forge) this information. diff --git a/internal/licensing/licensing.go b/internal/licensing/licensing.go index 6236a9141e31d..e394ae19a5791 100644 --- a/internal/licensing/licensing.go +++ b/internal/licensing/licensing.go @@ -188,17 +188,10 @@ func GetConfiguredProductLicenseInfoWithSignature() (*Info, string, error) { return GetFreeLicenseInfo(), "", nil } -// licenseGenerationPrivateKeyURL is the URL where Sourcegraph staff can find the private key for -// generating licenses. -// -// NOTE: If you change this, use text search to replace other instances of it (in source code -// comments). -const licenseGenerationPrivateKeyURL = "https://team-sourcegraph.1password.com/vaults/dnrhbauihkhjs5ag6vszsme45a/allitems/zkdx6gpw4uqejs3flzj7ef5j4i" - // envLicenseGenerationPrivateKey (the env var SOURCEGRAPH_LICENSE_GENERATION_KEY) is the // PEM-encoded form of the private key used to sign product license keys. It is stored at // https://team-sourcegraph.1password.com/vaults/dnrhbauihkhjs5ag6vszsme45a/allitems/zkdx6gpw4uqejs3flzj7ef5j4i. -var envLicenseGenerationPrivateKey = env.Get("SOURCEGRAPH_LICENSE_GENERATION_KEY", "", "the PEM-encoded form of the private key used to sign product license keys ("+licenseGenerationPrivateKeyURL+")") +var envLicenseGenerationPrivateKey = env.Get("SOURCEGRAPH_LICENSE_GENERATION_KEY", "", "the PEM-encoded form of the private key used to sign product license keys ("+license.GenerationPrivateKeyURL+")") // licenseGenerationPrivateKey is the private key used to generate license keys. var licenseGenerationPrivateKey = func() ssh.Signer { @@ -221,7 +214,7 @@ func GenerateProductLicenseKey(info license.Info) (licenseKey string, version in const msg = "no product license generation private key was configured" if env.InsecureDev { // Show more helpful error message in local dev. - return "", 0, errors.Errorf("%s (for testing by Sourcegraph staff: set the SOURCEGRAPH_LICENSE_GENERATION_KEY env var to the key obtained at %s)", msg, licenseGenerationPrivateKeyURL) + return "", 0, errors.Errorf("%s (for testing by Sourcegraph staff: set the SOURCEGRAPH_LICENSE_GENERATION_KEY env var to the key obtained at %s)", msg, license.GenerationPrivateKeyURL) } return "", 0, errors.New(msg) } diff --git a/lib/enterpriseportal/subscriptions/v1/subscriptions.pb.go b/lib/enterpriseportal/subscriptions/v1/subscriptions.pb.go index a2b03b076c83d..eb139fbd2130d 100644 --- a/lib/enterpriseportal/subscriptions/v1/subscriptions.pb.go +++ b/lib/enterpriseportal/subscriptions/v1/subscriptions.pb.go @@ -540,6 +540,9 @@ type EnterpriseSubscriptionLicenseKey struct { Info *EnterpriseSubscriptionLicenseKey_Info `protobuf:"bytes,2,opt,name=info,proto3" json:"info,omitempty"` // The signed license key. LicenseKey string `protobuf:"bytes,3,opt,name=license_key,json=licenseKey,proto3" json:"license_key,omitempty"` + // Generated display name representing the plan and some high-level attributes + // about the plan. + PlanDisplayName string `protobuf:"bytes,4,opt,name=plan_display_name,json=planDisplayName,proto3" json:"plan_display_name,omitempty"` } func (x *EnterpriseSubscriptionLicenseKey) Reset() { @@ -595,6 +598,13 @@ func (x *EnterpriseSubscriptionLicenseKey) GetLicenseKey() string { return "" } +func (x *EnterpriseSubscriptionLicenseKey) GetPlanDisplayName() string { + if x != nil { + return x.PlanDisplayName + } + return "" +} + type EnterpriseSubscriptionLicenseCondition struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1453,6 +1463,8 @@ type CreateEnterpriseSubscriptionLicenseRequest struct { // - license.key.info.expire_time // - license.key.info.salesforce_opportunity_id License *EnterpriseSubscriptionLicense `protobuf:"bytes,1,opt,name=license,proto3" json:"license,omitempty"` + // Message to associate with the license creation event. + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` } func (x *CreateEnterpriseSubscriptionLicenseRequest) Reset() { @@ -1494,6 +1506,13 @@ func (x *CreateEnterpriseSubscriptionLicenseRequest) GetLicense() *EnterpriseSub return nil } +func (x *CreateEnterpriseSubscriptionLicenseRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + type CreateEnterpriseSubscriptionLicenseResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1652,6 +1671,8 @@ type UpdateEnterpriseSubscriptionRequest struct { // Updatable fields are: // - instance_domain // - display_name + // - salesforce.subscription_id + // - salesforce.opportunity_id UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` } @@ -1858,6 +1879,8 @@ type CreateEnterpriseSubscriptionRequest struct { // - instance_domain // - salesforce.subscription_id Subscription *EnterpriseSubscription `protobuf:"bytes,1,opt,name=subscription,proto3" json:"subscription,omitempty"` + // Message to associate with the subscription creation event. + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` } func (x *CreateEnterpriseSubscriptionRequest) Reset() { @@ -1899,6 +1922,13 @@ func (x *CreateEnterpriseSubscriptionRequest) GetSubscription() *EnterpriseSubsc return nil } +func (x *CreateEnterpriseSubscriptionRequest) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + type CreateEnterpriseSubscriptionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2327,7 +2357,7 @@ var file_subscriptions_proto_rawDesc = []byte{ 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0a, 0x73, 0x61, 0x6c, - 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0xb7, 0x03, 0x0a, 0x20, 0x45, 0x6e, 0x74, 0x65, + 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0xe3, 0x03, 0x0a, 0x20, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x66, 0x6f, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, @@ -2339,233 +2369,239 @@ var file_subscriptions_proto_rawDesc = []byte{ 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x2e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x1a, 0xf0, - 0x01, 0x0a, 0x04, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x75, - 0x73, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x09, 0x75, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x65, 0x78, - 0x70, 0x69, 0x72, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, - 0x69, 0x72, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x1a, 0x73, 0x61, 0x6c, 0x65, 0x73, - 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x73, 0x61, 0x6c, - 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, - 0x72, 0x63, 0x65, 0x5f, 0x6f, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x5f, - 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, - 0x6f, 0x72, 0x63, 0x65, 0x4f, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x49, - 0x64, 0x22, 0xc4, 0x02, 0x0a, 0x26, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, - 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, - 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x14, - 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, - 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x68, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x50, 0x2e, 0x65, 0x6e, 0x74, - 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, - 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x48, - 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, - 0x45, 0x44, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, - 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x02, 0x22, 0xa7, 0x02, 0x0a, 0x1d, 0x45, 0x6e, 0x74, - 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x12, 0x69, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, + 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x2a, + 0x0a, 0x11, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x44, + 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0xf0, 0x01, 0x0a, 0x04, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x75, 0x73, 0x65, + 0x72, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3b, 0x0a, 0x0b, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72, 0x65, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x1a, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, + 0x65, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, + 0x72, 0x63, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x64, 0x12, 0x3a, 0x0a, 0x19, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, + 0x6f, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x4f, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x49, 0x64, 0x22, 0xc4, 0x02, + 0x0a, 0x26, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4c, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x68, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x50, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, + 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x48, 0x0a, 0x06, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x12, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, + 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x56, 0x4f, 0x4b, + 0x45, 0x44, 0x10, 0x02, 0x22, 0xa7, 0x02, 0x0a, 0x1d, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, + 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x69, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x49, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, + 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, + 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x57, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x57, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x65, 0x6e, - 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, - 0x48, 0x00, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x42, 0x09, 0x0a, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, - 0x73, 0x65, 0x22, 0x3d, 0x0a, 0x20, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, - 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x00, 0x52, 0x02, 0x69, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, - 0x79, 0x22, 0x82, 0x01, 0x0a, 0x21, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, - 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, - 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, - 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x8b, 0x03, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x45, - 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x0f, - 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x61, 0x72, - 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0a, - 0x69, 0x73, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x4f, 0x0a, 0x0a, 0x70, 0x65, - 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, - 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, - 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, - 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0c, 0x64, - 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x6d, 0x0a, 0x0a, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, + 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x48, 0x00, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x42, 0x09, 0x0a, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x22, 0x3d, + 0x0a, 0x20, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x10, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x02, 0x69, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x22, 0x82, 0x01, + 0x0a, 0x21, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x65, 0x6e, 0x74, 0x65, + 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, + 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0x8b, 0x03, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, + 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x61, 0x72, 0x63, 0x68, 0x69, 0x76, + 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0a, 0x69, 0x73, 0x41, 0x72, + 0x63, 0x68, 0x69, 0x76, 0x65, 0x64, 0x12, 0x4f, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x65, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x0a, 0x70, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x70, 0x6c, + 0x61, 0x79, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x6d, 0x0a, 0x0a, + 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x4b, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x61, 0x6c, 0x65, 0x73, + 0x66, 0x6f, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, + 0x0a, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, 0x29, 0x0a, 0x0f, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x42, 0x08, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x22, 0xc0, 0x01, 0x0a, 0x22, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, + 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, + 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x12, 0x5e, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, - 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x53, - 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x48, 0x00, 0x52, 0x0a, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x12, - 0x29, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x64, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, - 0x61, 0x6e, 0x63, 0x65, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x42, 0x08, 0x0a, 0x06, 0x66, 0x69, - 0x6c, 0x74, 0x65, 0x72, 0x22, 0xc0, 0x01, 0x0a, 0x22, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, - 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, - 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x5e, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, - 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x07, - 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x22, 0xae, 0x01, 0x0a, 0x23, 0x4c, 0x69, 0x73, 0x74, - 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, - 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, - 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x5f, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x39, - 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, - 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd0, 0x02, 0x0a, 0x28, 0x4c, 0x69, 0x73, - 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x46, - 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, - 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x12, 0x5a, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x44, - 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, - 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x48, 0x00, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0a, - 0x69, 0x73, 0x5f, 0x72, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x48, 0x00, 0x52, 0x09, 0x69, 0x73, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x12, 0x34, 0x0a, - 0x15, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x75, 0x62, - 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x13, - 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x53, 0x75, 0x62, 0x73, 0x74, 0x72, - 0x69, 0x6e, 0x67, 0x12, 0x3c, 0x0a, 0x19, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, - 0x65, 0x5f, 0x6f, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x17, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, - 0x6f, 0x72, 0x63, 0x65, 0x4f, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x49, - 0x64, 0x42, 0x08, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xce, 0x01, 0x0a, 0x29, - 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, - 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, - 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, - 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, - 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, - 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x65, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, - 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, - 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x46, 0x69, 0x6c, - 0x74, 0x65, 0x72, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x22, 0xb2, 0x01, 0x0a, - 0x2a, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, - 0x73, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6e, + 0x69, 0x6f, 0x6e, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x74, + 0x65, 0x72, 0x73, 0x22, 0xae, 0x01, 0x0a, 0x23, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x65, + 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x12, 0x5c, 0x0a, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, - 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, - 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, - 0x73, 0x22, 0x88, 0x01, 0x0a, 0x2a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, - 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x5a, 0x0a, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x6b, 0x65, 0x6e, 0x12, 0x5f, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x65, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, + 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xd0, 0x02, 0x0a, 0x28, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, + 0x72, 0x12, 0x29, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x73, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x5a, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x44, 0x2e, 0x65, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, + 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, + 0x48, 0x00, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x72, + 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, + 0x69, 0x73, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x64, 0x12, 0x34, 0x0a, 0x15, 0x6c, 0x69, 0x63, + 0x65, 0x6e, 0x73, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x13, 0x6c, 0x69, 0x63, 0x65, + 0x6e, 0x73, 0x65, 0x4b, 0x65, 0x79, 0x53, 0x75, 0x62, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, + 0x3c, 0x0a, 0x19, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x6f, 0x70, + 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x00, 0x52, 0x17, 0x73, 0x61, 0x6c, 0x65, 0x73, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x4f, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x75, 0x6e, 0x69, 0x74, 0x79, 0x49, 0x64, 0x42, 0x08, 0x0a, + 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xce, 0x01, 0x0a, 0x29, 0x4c, 0x69, 0x73, 0x74, + 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, + 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x65, 0x0a, 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, + 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x52, + 0x07, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x22, 0xb2, 0x01, 0x0a, 0x2a, 0x4c, 0x69, 0x73, + 0x74, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, + 0x5c, 0x0a, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, - 0x6e, 0x73, 0x65, 0x52, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x22, 0x89, 0x01, 0x0a, - 0x2b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, - 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, - 0x65, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x07, + 0x6e, 0x73, 0x65, 0x52, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0xa2, 0x01, + 0x0a, 0x2a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, + 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, + 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5a, 0x0a, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, - 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x22, 0x63, 0x0a, 0x2a, 0x52, 0x65, 0x76, 0x6f, - 0x6b, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, - 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6c, 0x69, 0x63, 0x65, - 0x6e, 0x73, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x2d, 0x0a, - 0x2b, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, - 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, - 0x65, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xc1, 0x01, 0x0a, - 0x23, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, - 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x65, 0x6e, 0x74, - 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, - 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, - 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, - 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, - 0x22, 0x85, 0x01, 0x0a, 0x24, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x39, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, - 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x67, 0x0a, 0x24, 0x41, 0x72, 0x63, 0x68, - 0x69, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, - 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x22, 0x27, 0x0a, 0x25, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x65, - 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x84, 0x01, 0x0a, 0x23, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, - 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, + 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x89, 0x01, 0x0a, 0x2b, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, + 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, + 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x07, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x22, 0x63, + 0x0a, 0x2a, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, + 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, + 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x22, 0x2d, 0x0a, 0x2b, 0x52, 0x65, 0x76, 0x6f, 0x6b, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0x85, 0x01, 0x0a, 0x24, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, + 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0xc1, 0x01, 0x0a, 0x23, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, + 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x39, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x75, 0x62, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x0b, 0x75, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x4d, 0x61, 0x73, 0x6b, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x4d, 0x61, 0x73, 0x6b, 0x22, 0x85, 0x01, 0x0a, 0x24, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, + 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, + 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x67, + 0x0a, 0x24, 0x41, 0x72, 0x63, 0x68, 0x69, 0x76, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, + 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x27, 0x0a, 0x25, 0x41, 0x72, 0x63, 0x68, 0x69, + 0x76, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x9e, 0x01, 0x0a, 0x23, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, + 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, + 0x2e, 0x65, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x61, + 0x6c, 0x2e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x22, 0x85, 0x01, 0x0a, 0x24, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, diff --git a/lib/enterpriseportal/subscriptions/v1/subscriptions.proto b/lib/enterpriseportal/subscriptions/v1/subscriptions.proto index b3b53d060cac0..2c76486fd6d21 100644 --- a/lib/enterpriseportal/subscriptions/v1/subscriptions.proto +++ b/lib/enterpriseportal/subscriptions/v1/subscriptions.proto @@ -145,6 +145,9 @@ message EnterpriseSubscriptionLicenseKey { Info info = 2; // The signed license key. string license_key = 3; + // Generated display name representing the plan and some high-level attributes + // about the plan. + string plan_display_name = 4; } message EnterpriseSubscriptionLicenseCondition { @@ -319,6 +322,9 @@ message CreateEnterpriseSubscriptionLicenseRequest { // - license.key.info.expire_time // - license.key.info.salesforce_opportunity_id EnterpriseSubscriptionLicense license = 1; + + // Message to associate with the license creation event. + string message = 2; } message CreateEnterpriseSubscriptionLicenseResponse { @@ -346,6 +352,8 @@ message UpdateEnterpriseSubscriptionRequest { // Updatable fields are: // - instance_domain // - display_name + // - salesforce.subscription_id + // - salesforce.opportunity_id google.protobuf.FieldMask update_mask = 2; } @@ -373,6 +381,9 @@ message CreateEnterpriseSubscriptionRequest { // - instance_domain // - salesforce.subscription_id EnterpriseSubscription subscription = 1; + + // Message to associate with the subscription creation event. + string message = 2; } message CreateEnterpriseSubscriptionResponse {