feat/enterpriseportal: more list options for subscriptions and licenses (#64114)

Required to build an updated subscriptions management UI.

Most of the diff is generated proto for some reason

Closes https://linear.app/sourcegraph/issue/CORE-226

## Test plan

Integration tests
This commit is contained in:
Robert Lin 2024-08-09 09:43:39 -07:00 committed by GitHub
parent 14a4d7ce31
commit 0de249daf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 983 additions and 678 deletions

View File

@ -132,9 +132,10 @@ func NewLicensesStore(db *pgxpool.Pool) *LicensesStore {
}
type ListLicensesOpts struct {
SubscriptionID string
LicenseType subscriptionsv1.EnterpriseSubscriptionLicenseType
LicenseKeySubstring string
SubscriptionID string
LicenseType subscriptionsv1.EnterpriseSubscriptionLicenseType
LicenseKeySubstring string
SalesforceOpportunityID string
// PageSize is the maximum number of licenses to return.
PageSize int
}
@ -151,11 +152,21 @@ func (opts ListLicensesOpts) toQueryConditions() (where, limitClause string, _ p
"license_type = @licenseType")
namedArgs["licenseType"] = opts.LicenseType.String()
}
if opts.LicenseKeySubstring != "" {
whereConds = append(whereConds,
"license_data->>'SignedKey' LIKE '%' || @licenseKeySubstring || '%'")
namedArgs["licenseKeySubstring"] = opts.LicenseKeySubstring
switch opts.LicenseType {
case subscriptionsv1.EnterpriseSubscriptionLicenseType_ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY:
if opts.LicenseKeySubstring != "" {
whereConds = append(whereConds,
"license_data->>'SignedKey' LIKE '%' || @licenseKeySubstring || '%'")
namedArgs["licenseKeySubstring"] = opts.LicenseKeySubstring
}
if opts.SalesforceOpportunityID != "" {
whereConds = append(whereConds,
"license_data->'Info'->>'sf_opp_id' = @salesforceOpportunityID")
namedArgs["salesforceOpportunityID"] = opts.SalesforceOpportunityID
}
}
where = strings.Join(whereConds, " AND ")
if opts.PageSize > 0 {

View File

@ -124,6 +124,8 @@ func TestLicensesStore(t *testing.T) {
Tags: []string{"tag"},
CreatedAt: time.Time{}.Add(24 * time.Hour),
ExpiresAt: time.Time{}.Add(48 * time.Hour),
SalesforceOpportunityID: pointers.Ptr("sf_opportunity"),
},
SignedKey: "asdffdsadf",
},
@ -136,7 +138,7 @@ func TestLicensesStore(t *testing.T) {
testLicense(
got,
autogold.Expect(valast.Ptr("TestLicensesStore/CreateLicenseKey 2")),
autogold.Expect(`{"Info": {"c": "0001-01-02T00:00:00Z", "e": "0001-01-03T00:00:00Z", "t": ["tag"], "u": 0}, "SignedKey": "asdffdsadf"}`),
autogold.Expect(`{"Info": {"c": "0001-01-02T00:00:00Z", "e": "0001-01-03T00:00:00Z", "t": ["tag"], "u": 0, "sf_opp_id": "sf_opportunity"}, "SignedKey": "asdffdsadf"}`),
)
createdLicenses = append(createdLicenses, got)
@ -229,6 +231,34 @@ func TestLicensesStore(t *testing.T) {
require.NoError(t, err)
require.Len(t, listedLicenses, 1)
assert.Equal(t, subscriptionID1, listedLicenses[0].SubscriptionID)
t.Run("no match", func(t *testing.T) {
listedLicenses, err = licenses.List(ctx, subscriptions.ListLicensesOpts{
LicenseType: subscriptionsv1.EnterpriseSubscriptionLicenseType_ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY,
LicenseKeySubstring: "no-match",
})
require.NoError(t, err)
assert.Len(t, listedLicenses, 0)
})
})
t.Run("List by salesforce opportunity ID", func(t *testing.T) {
listedLicenses, err := licenses.List(ctx, subscriptions.ListLicensesOpts{
LicenseType: subscriptionsv1.EnterpriseSubscriptionLicenseType_ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY,
SalesforceOpportunityID: "sf_opportunity",
})
require.NoError(t, err)
require.Len(t, listedLicenses, 1)
assert.Equal(t, subscriptionID2, listedLicenses[0].SubscriptionID)
t.Run("no match", func(t *testing.T) {
listedLicenses, err := licenses.List(ctx, subscriptions.ListLicensesOpts{
LicenseType: subscriptionsv1.EnterpriseSubscriptionLicenseType_ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY,
SalesforceOpportunityID: "no-match",
})
require.NoError(t, err)
assert.Len(t, listedLicenses, 0)
})
})
})

View File

@ -125,7 +125,13 @@ type ListEnterpriseSubscriptionsOptions struct {
InstanceDomains []string
// IsArchived indicates whether to only list archived subscriptions, or only
// non-archived subscriptions.
IsArchived bool
IsArchived *bool
// DisplayNameSubstring is a substring match on display name.
DisplayNameSubstring string
// SalesforceSubscriptionIDs are exact matches on the Salesforce subscription
// ID.
SalesforceSubscriptionIDs []string
// PageSize is the maximum number of subscriptions to return.
PageSize int
}
@ -141,10 +147,24 @@ func (opts ListEnterpriseSubscriptionsOptions) toQueryConditions() (where, limit
whereConds = append(whereConds, "instance_domain = ANY(@instanceDomains)")
namedArgs["instanceDomains"] = opts.InstanceDomains
}
// Future: Uncomment the following block when the archived field is added to the table.
// if opts.OnlyArchived {
// whereConds = append(whereConds, "archived = TRUE")
// }
if opts.IsArchived != nil {
if *opts.IsArchived {
whereConds = append(whereConds, "archived_at IS NOT NULL")
} else {
whereConds = append(whereConds, "archived IS NUlL")
}
}
if len(opts.DisplayNameSubstring) > 0 {
whereConds = append(whereConds,
"LOWER(display_name) LIKE '%' || LOWER(@displayName) || '%'")
namedArgs["displayName"] = opts.DisplayNameSubstring
}
if len(opts.SalesforceSubscriptionIDs) > 0 {
whereConds = append(whereConds,
"salesforce_subscription_id = ANY(@salesforceSubscriptionIDs)")
namedArgs["salesforceSubscriptionIDs"] = opts.SalesforceSubscriptionIDs
}
where = strings.Join(whereConds, " AND ")
if opts.PageSize > 0 {

View File

@ -59,7 +59,9 @@ func SubscriptionsStoreList(t *testing.T, ctx context.Context, s *subscriptions.
ctx,
uuid.New().String(),
subscriptions.UpsertSubscriptionOptions{
InstanceDomain: database.NewNullString("s1.sourcegraph.com"),
DisplayName: database.NewNullString("Subscription 1"),
InstanceDomain: database.NewNullString("s1.sourcegraph.com"),
SalesforceSubscriptionID: pointers.Ptr("sf_sub_id"),
},
)
require.NoError(t, err)
@ -67,20 +69,24 @@ func SubscriptionsStoreList(t *testing.T, ctx context.Context, s *subscriptions.
ctx,
uuid.New().String(),
subscriptions.UpsertSubscriptionOptions{
DisplayName: database.NewNullString("Subscription 2"),
InstanceDomain: database.NewNullString("s2.sourcegraph.com"),
},
)
require.NoError(t, err)
_, err = s.Upsert(
s3, err := s.Upsert(
ctx,
uuid.New().String(),
subscriptions.UpsertSubscriptionOptions{
DisplayName: database.NewNullString("Subscription 3"),
InstanceDomain: database.NewNullString("s3.sourcegraph.com"),
},
)
require.NoError(t, err)
t.Run("list by IDs", func(t *testing.T) {
t.Parallel()
ss, err := s.List(ctx, subscriptions.ListEnterpriseSubscriptionsOptions{IDs: []string{s1.ID, s2.ID}})
require.NoError(t, err)
require.Len(t, ss, 2)
@ -101,6 +107,8 @@ func SubscriptionsStoreList(t *testing.T, ctx context.Context, s *subscriptions.
})
t.Run("list by instance domains", func(t *testing.T) {
t.Parallel()
ss, err := s.List(ctx, subscriptions.ListEnterpriseSubscriptionsOptions{
InstanceDomains: []string{*s1.InstanceDomain, *s2.InstanceDomain}},
)
@ -122,7 +130,78 @@ func SubscriptionsStoreList(t *testing.T, ctx context.Context, s *subscriptions.
})
})
t.Run("list by display name", func(t *testing.T) {
t.Parallel()
ss, err := s.List(
ctx,
subscriptions.ListEnterpriseSubscriptionsOptions{
DisplayNameSubstring: "Subscription",
},
)
require.NoError(t, err)
assert.Len(t, ss, 3) // all 3 are returned
t.Run("single match", func(t *testing.T) {
t.Parallel()
ss, err := s.List(
ctx,
subscriptions.ListEnterpriseSubscriptionsOptions{
DisplayNameSubstring: "tion 3",
},
)
require.NoError(t, err)
assert.Len(t, ss, 1)
assert.Equal(t, s3.ID, ss[0].ID)
})
t.Run("exact match", func(t *testing.T) {
t.Parallel()
ss, err := s.List(
ctx,
subscriptions.ListEnterpriseSubscriptionsOptions{
DisplayNameSubstring: "Subscription 2",
},
)
require.NoError(t, err)
assert.Len(t, ss, 1)
assert.Equal(t, s2.ID, ss[0].ID)
})
t.Run("case-insensitive match", func(t *testing.T) {
t.Parallel()
ss, err := s.List(
ctx,
subscriptions.ListEnterpriseSubscriptionsOptions{
DisplayNameSubstring: "subscription 2",
},
)
require.NoError(t, err)
assert.Len(t, ss, 1)
assert.Equal(t, s2.ID, ss[0].ID)
})
})
t.Run("list by Salesforce subscription ID", func(t *testing.T) {
t.Parallel()
ss, err := s.List(
ctx,
subscriptions.ListEnterpriseSubscriptionsOptions{
SalesforceSubscriptionIDs: []string{"sf_sub_id"},
},
)
require.NoError(t, err)
assert.Len(t, ss, 1)
assert.Equal(t, s1.ID, ss[0].ID)
})
t.Run("list with page size", func(t *testing.T) {
t.Parallel()
ss, err := s.List(
ctx,
subscriptions.ListEnterpriseSubscriptionsOptions{

View File

@ -11,6 +11,7 @@ import (
"github.com/sourcegraph/sourcegraph/internal/redislock"
"github.com/sourcegraph/sourcegraph/internal/redispool"
"github.com/sourcegraph/sourcegraph/internal/slack"
"github.com/sourcegraph/sourcegraph/lib/pointers"
)
type Store interface {
@ -72,7 +73,7 @@ func (s *storeHandle) TryAcquireJob(ctx context.Context) (acquired bool, release
func (s *storeHandle) ListSubscriptions(ctx context.Context) ([]*subscriptions.SubscriptionWithConditions, error) {
return s.subscriptions.List(ctx, subscriptions.ListEnterpriseSubscriptionsOptions{
IsArchived: false,
IsArchived: pointers.Ptr(false),
})
}

View File

@ -68,9 +68,9 @@ func convertSubscriptionToProto(subscription *subscriptions.SubscriptionWithCond
}
}
var sf *subscriptionsv1.EnterpriseSubscription_SalesforceMetadata
var sf *subscriptionsv1.EnterpriseSubscriptionSalesforceMetadata
if subscription.SalesforceSubscriptionID != nil {
sf = &subscriptionsv1.EnterpriseSubscription_SalesforceMetadata{
sf = &subscriptionsv1.EnterpriseSubscriptionSalesforceMetadata{
SubscriptionId: pointers.DerefZero(subscription.SalesforceSubscriptionID),
}
}

View File

@ -74,8 +74,13 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec
// Validate and process filters.
filters := req.Msg.GetFilters()
isArchived := false
subscriptionIDs := make(collections.Set[string], len(filters))
var (
isArchived *bool
subscriptionIDs = make(collections.Set[string], len(filters))
displayNameSubstring string
salesforceSubscriptionIDs []string
instanceDomains []string
)
var iamListObjectOptions *iam.ListObjectsOptions
for _, filter := range filters {
switch f := filter.GetFilter().(type) {
@ -89,7 +94,7 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec
subscriptionIDs.Add(
strings.TrimPrefix(f.SubscriptionId, subscriptionsv1.EnterpriseSubscriptionIDPrefix))
case *subscriptionsv1.ListEnterpriseSubscriptionsFilter_IsArchived:
isArchived = f.IsArchived
isArchived = &f.IsArchived
case *subscriptionsv1.ListEnterpriseSubscriptionsFilter_Permission:
if f.Permission == nil {
return nil, connect.NewError(
@ -130,6 +135,39 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec
errors.Wrap(err, `invalid filter: "permission" provided but invalid`),
)
}
case *subscriptionsv1.ListEnterpriseSubscriptionsFilter_DisplayName:
if displayNameSubstring != "" {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.Newf(`invalid filter: "display_name" provided more than once`),
)
}
const minLength = 3
if len(f.DisplayName) < minLength {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.Newf(`invalid filter: "display_name" must be longer than %d characters`, minLength),
)
}
displayNameSubstring = f.DisplayName
case *subscriptionsv1.ListEnterpriseSubscriptionsFilter_Salesforce:
if f.Salesforce.SubscriptionId == "" {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.Newf(`invalid filter: "salesforce.subscription_id" is empty`),
)
}
salesforceSubscriptionIDs = append(salesforceSubscriptionIDs,
f.Salesforce.SubscriptionId)
case *subscriptionsv1.ListEnterpriseSubscriptionsFilter_InstanceDomain:
domain, err := subscriptionsv1.NormalizeInstanceDomain(f.InstanceDomain)
if err != nil {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.Wrap(err, `invalid filter: "domain" provided but invalid`),
)
}
instanceDomains = append(instanceDomains, domain)
}
}
@ -165,9 +203,13 @@ func (s *handlerV1) ListEnterpriseSubscriptions(ctx context.Context, req *connec
subs, err := s.store.ListEnterpriseSubscriptions(
ctx,
subscriptions.ListEnterpriseSubscriptionsOptions{
IDs: subscriptionIDs.Values(),
IsArchived: isArchived,
PageSize: int(req.Msg.GetPageSize()),
IDs: subscriptionIDs.Values(),
IsArchived: isArchived,
InstanceDomains: instanceDomains,
DisplayNameSubstring: displayNameSubstring,
SalesforceSubscriptionIDs: salesforceSubscriptionIDs,
PageSize: int(req.Msg.GetPageSize()),
},
)
if err != nil {
@ -236,10 +278,11 @@ func (s *handlerV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, req
opts.LicenseType = f.Type
case *subscriptionsv1.ListEnterpriseSubscriptionLicensesFilter_LicenseKeySubstring:
if f.LicenseKeySubstring == "" {
const minLength = 3
if len(f.LicenseKeySubstring) < minLength {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.New(`invalid filter: "license_key_substring" is provided but is empty`),
errors.Newf(`invalid filter: "license_key_substring" must be longer than %d characters`, minLength),
)
}
if opts.LicenseKeySubstring != "" {
@ -264,15 +307,31 @@ func (s *handlerV1) ListEnterpriseSubscriptionLicenses(ctx context.Context, req
)
}
opts.SubscriptionID = f.SubscriptionId
case *subscriptionsv1.ListEnterpriseSubscriptionLicensesFilter_SalesforceOpportunityId:
if f.SalesforceOpportunityId == "" {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.New(`invalid filter: "salesforce_opportunity_id" provided but is empty`),
)
}
opts.SalesforceOpportunityID = f.SalesforceOpportunityId
}
}
if opts.LicenseType != subscriptionsv1.EnterpriseSubscriptionLicenseType_ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY &&
opts.LicenseKeySubstring != "" {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.New(`invalid filters: "license_type" must be 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' to use the "license_key_substring" filter`),
)
if opts.LicenseType != subscriptionsv1.EnterpriseSubscriptionLicenseType_ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY {
if opts.LicenseKeySubstring != "" {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.New(`invalid filters: "license_type" must be 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' to use the "license_key_substring" filter`),
)
}
if opts.SalesforceOpportunityID != "" {
return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.New(`invalid filters: "license_type" must be 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY' to use the "salesforce_opportunity_id" filter`),
)
}
}
licenses, err := s.store.ListEnterpriseSubscriptionLicenses(ctx, opts)

File diff suppressed because it is too large Load Diff

View File

@ -95,6 +95,11 @@ message EnterpriseSubscriptionCondition {
string message = 3;
}
message EnterpriseSubscriptionSalesforceMetadata {
// The Salesforce subscription ID associated with this Enterprise subscription.
string subscription_id = 1;
}
// EnterpriseSubscription represents a Sourcegraph Enterprise subscription.
message EnterpriseSubscription {
// ID is the external, prefixed UUID-format identifier for this subscription
@ -107,12 +112,8 @@ message EnterpriseSubscription {
// The instance domain associated with this subscription, e.g. "acme.sourcegraphcloud.com".
string instance_domain = 4;
message SalesforceMetadata {
// The Salesforce subscription ID associated with this Enterprise subscription.
string subscription_id = 1;
}
// Salesforce details associated with this subscription.
SalesforceMetadata salesforce = 5;
EnterpriseSubscriptionSalesforceMetadata salesforce = 5;
}
// EnterpriseSubscriptionLicenseKey is the classic offline Sourcegraph license
@ -208,12 +209,20 @@ enum EnterpriseSubscriptionLicenseType {
message ListEnterpriseSubscriptionsFilter {
oneof filter {
// Return only product subscriptions has the given subscription ID.
// Filter by exact match on the Enterprise Subscription ID.
string subscription_id = 1;
// Return only product subscriptions with the given archival status.
bool is_archived = 2;
// Return only product subscriptions that satisfies the given permission.
Permission permission = 3;
// Filter by partial match on display name. The query must be at least 3
// characters long.
string display_name = 4;
// Filter by exact match on Salesforce metadata.
EnterpriseSubscriptionSalesforceMetadata salesforce = 5;
// Filter by partial match on instance domain. The query must be a valid
// domain.
string instance_domain = 6;
}
}
@ -252,9 +261,15 @@ message ListEnterpriseSubscriptionLicensesFilter {
EnterpriseSubscriptionLicenseType type = 2;
// Return only licenses that are active.
bool is_revoked = 3;
// Return only product subscriptions whose license key contains this
// substring.
// Return only licenses where the signed license key contains this substring.
// Query must be at least 3 characters.
//
// MUST be used in conjunction with the type = 'ENTERPRISE_SUBSCRIPTION_LICENSE_TYPE_KEY'
// filter.
string license_key_substring = 4;
// Return only licenses associated with this Salesforce opportunity ID.
// MUST be used in conjunction with the 'type' filter.
string salesforce_opportunity_id = 5;
}
}