feat/enterpriseportal: all subscriptions APIs use enterprise portal DB (#63959)

This change follows
https://github.com/sourcegraph/sourcegraph/pull/63858 by making the
_all_ subscriptions APIs read and write to the Enterprise Portal
database, instead of dotcomdb, using the data that we sync from dotcomdb
into Enterprise Portal.

With this PR, all initially proposed subscriptions APIs are at least
partially implemented.

Uses https://github.com/hexops/valast/pull/27 for custom `autogold`
rendering of `utctime.Time`

Closes https://linear.app/sourcegraph/issue/CORE-156
Part of https://linear.app/sourcegraph/issue/CORE-158

## Test plan

- [x] Unit tests on API level
- [x] Adapters unit testing
- [x] Simple E2E test:
https://github.com/sourcegraph/sourcegraph/pull/64057
This commit is contained in:
Robert Lin 2024-08-09 17:26:18 -07:00 committed by GitHub
parent 8296e9804f
commit e2c646ad92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 2921 additions and 317 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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) UpdateEnterpriseSubscription(ctx context.Context, req *connect.Request[subscriptionsv1.UpdateEnterpriseSubscriptionRequest]) (*connect.Response[subscriptionsv1.UpdateEnterpriseSubscriptionResponse], error) {
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("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)
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(
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.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 {

View File

@ -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) UpsertEnterpriseSubscription(ctx context.Context, subscriptionID string, opts subscriptions.UpsertSubscriptionOptions) (*subscriptions.SubscriptionWithConditions, error) {
return s.db.Subscriptions().Upsert(ctx, subscriptionID, opts)
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, 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

2
go.mod
View File

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

5
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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