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 {