diff --git a/codersdk/features.go b/codersdk/features.go index 58fff94e0aace..916df4db6110d 100644 --- a/codersdk/features.go +++ b/codersdk/features.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "net/http" + "strings" ) type Entitlement string @@ -14,19 +15,24 @@ const ( EntitlementNotEntitled Entitlement = "not_entitled" ) +// To add a new feature, modify this set of enums as well as the FeatureNames +// array below. +type FeatureName string + const ( - FeatureUserLimit = "user_limit" - FeatureAuditLog = "audit_log" - FeatureBrowserOnly = "browser_only" - FeatureSCIM = "scim" - FeatureTemplateRBAC = "template_rbac" - FeatureHighAvailability = "high_availability" - FeatureMultipleGitAuth = "multiple_git_auth" - FeatureExternalProvisionerDaemons = "external_provisioner_daemons" - FeatureAppearance = "appearance" + FeatureUserLimit FeatureName = "user_limit" + FeatureAuditLog FeatureName = "audit_log" + FeatureBrowserOnly FeatureName = "browser_only" + FeatureSCIM FeatureName = "scim" + FeatureTemplateRBAC FeatureName = "template_rbac" + FeatureHighAvailability FeatureName = "high_availability" + FeatureMultipleGitAuth FeatureName = "multiple_git_auth" + FeatureExternalProvisionerDaemons FeatureName = "external_provisioner_daemons" + FeatureAppearance FeatureName = "appearance" ) -var FeatureNames = []string{ +// FeatureNames must be kept in-sync with the Feature enum above. +var FeatureNames = []FeatureName{ FeatureUserLimit, FeatureAuditLog, FeatureBrowserOnly, @@ -38,6 +44,29 @@ var FeatureNames = []string{ FeatureAppearance, } +// Humanize returns the feature name in a human-readable format. +func (n FeatureName) Humanize() string { + switch n { + case FeatureTemplateRBAC: + return "Template RBAC" + case FeatureSCIM: + return "SCIM" + default: + return strings.Title(strings.ReplaceAll(string(n), "_", " ")) + } +} + +// AlwaysEnable returns if the feature is always enabled if entitled. +// Warning: We don't know if we need this functionality. +// This method may disappear at any time. +func (n FeatureName) AlwaysEnable() bool { + return map[FeatureName]bool{ + FeatureMultipleGitAuth: true, + FeatureExternalProvisionerDaemons: true, + FeatureAppearance: true, + }[n] +} + type Feature struct { Entitlement Entitlement `json:"entitlement"` Enabled bool `json:"enabled"` @@ -46,12 +75,12 @@ type Feature struct { } type Entitlements struct { - Features map[string]Feature `json:"features"` - Warnings []string `json:"warnings"` - Errors []string `json:"errors"` - HasLicense bool `json:"has_license"` - Experimental bool `json:"experimental"` - Trial bool `json:"trial"` + Features map[FeatureName]Feature `json:"features"` + Warnings []string `json:"warnings"` + Errors []string `json:"errors"` + HasLicense bool `json:"has_license"` + Experimental bool `json:"experimental"` + Trial bool `json:"trial"` } func (c *Client) Entitlements(ctx context.Context) (Entitlements, error) { diff --git a/codersdk/licenses.go b/codersdk/licenses.go index 3ede213e53251..59319a326e48e 100644 --- a/codersdk/licenses.go +++ b/codersdk/licenses.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + "golang.org/x/xerrors" ) type AddLicenseRequest struct { @@ -25,6 +26,30 @@ type License struct { Claims map[string]interface{} `json:"claims"` } +// Features provides the feature claims in license. +func (l *License) Features() (map[FeatureName]int64, error) { + strMap, ok := l.Claims["features"].(map[string]interface{}) + if !ok { + return nil, xerrors.New("features key is unexpected type") + } + fMap := make(map[FeatureName]int64) + for k, v := range strMap { + jn, ok := v.(json.Number) + if !ok { + return nil, xerrors.Errorf("feature %q has unexpected type", k) + } + + n, err := jn.Int64() + if err != nil { + return nil, err + } + + fMap[FeatureName(k)] = n + } + + return fMap, nil +} + func (c *Client) AddLicense(ctx context.Context, r AddLicenseRequest) (License, error) { res, err := c.Request(ctx, http.MethodPost, "/api/v2/licenses", r) if err != nil { diff --git a/enterprise/cli/features.go b/enterprise/cli/features.go index b0ced1277caac..3c2abfb13fb37 100644 --- a/enterprise/cli/features.go +++ b/enterprise/cli/features.go @@ -88,17 +88,17 @@ func featuresList() *cobra.Command { } type featureRow struct { - Name string `table:"name"` - Entitlement string `table:"entitlement"` - Enabled bool `table:"enabled"` - Limit *int64 `table:"limit"` - Actual *int64 `table:"actual"` + Name codersdk.FeatureName `table:"name"` + Entitlement string `table:"entitlement"` + Enabled bool `table:"enabled"` + Limit *int64 `table:"limit"` + Actual *int64 `table:"actual"` } // displayFeatures will return a table displaying all features passed in. // filterColumns must be a subset of the feature fields and will determine which // columns to display -func displayFeatures(filterColumns []string, features map[string]codersdk.Feature) (string, error) { +func displayFeatures(filterColumns []string, features map[codersdk.FeatureName]codersdk.Feature) (string, error) { rows := make([]featureRow, 0, len(features)) for name, feat := range features { rows = append(rows, featureRow{ diff --git a/enterprise/cli/groupcreate_test.go b/enterprise/cli/groupcreate_test.go index 2a0e61c6c0687..166214901b908 100644 --- a/enterprise/cli/groupcreate_test.go +++ b/enterprise/cli/groupcreate_test.go @@ -9,8 +9,10 @@ import ( "github.com/coder/coder/cli/clitest" "github.com/coder/coder/cli/cliui" "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/cli" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/pty/ptytest" ) @@ -23,7 +25,9 @@ func TestCreateGroup(t *testing.T) { client := coderdenttest.New(t, nil) coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) var ( diff --git a/enterprise/cli/groupdelete_test.go b/enterprise/cli/groupdelete_test.go index e2319a43e2aaa..a997af603f5f4 100644 --- a/enterprise/cli/groupdelete_test.go +++ b/enterprise/cli/groupdelete_test.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/cli" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/testutil" ) @@ -26,7 +27,9 @@ func TestGroupDelete(t *testing.T) { admin := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) @@ -57,7 +60,9 @@ func TestGroupDelete(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) cmd, root := clitest.NewWithSubcommands(t, cli.EnterpriseSubcommands(), diff --git a/enterprise/cli/groupedit_test.go b/enterprise/cli/groupedit_test.go index 8c4c8f0f16e49..f8581e607f68f 100644 --- a/enterprise/cli/groupedit_test.go +++ b/enterprise/cli/groupedit_test.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/cli" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/testutil" ) @@ -26,7 +27,9 @@ func TestGroupEdit(t *testing.T) { admin := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) @@ -77,7 +80,9 @@ func TestGroupEdit(t *testing.T) { admin := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) @@ -106,7 +111,9 @@ func TestGroupEdit(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) cmd, root := clitest.NewWithSubcommands(t, cli.EnterpriseSubcommands(), "groups", "edit") diff --git a/enterprise/cli/grouplist_test.go b/enterprise/cli/grouplist_test.go index 8740829a017c9..e806e1e52517e 100644 --- a/enterprise/cli/grouplist_test.go +++ b/enterprise/cli/grouplist_test.go @@ -10,6 +10,7 @@ import ( "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/cli" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/pty/ptytest" "github.com/coder/coder/testutil" ) @@ -24,7 +25,9 @@ func TestGroupList(t *testing.T) { admin := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) @@ -81,7 +84,9 @@ func TestGroupList(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) cmd, root := clitest.NewWithSubcommands(t, cli.EnterpriseSubcommands(), "groups", "list") diff --git a/enterprise/cli/licenses_test.go b/enterprise/cli/licenses_test.go index 924590dee984f..47617d8f914c7 100644 --- a/enterprise/cli/licenses_test.go +++ b/enterprise/cli/licenses_test.go @@ -338,7 +338,7 @@ func (s *fakeLicenseAPI) deleteLicense(rw http.ResponseWriter, r *http.Request) } func (*fakeLicenseAPI) entitlements(rw http.ResponseWriter, r *http.Request) { - features := make(map[string]codersdk.Feature) + features := make(map[codersdk.FeatureName]codersdk.Feature) for _, f := range codersdk.FeatureNames { features[f] = codersdk.Feature{ Entitlement: codersdk.EntitlementEntitled, diff --git a/enterprise/coderd/appearance_test.go b/enterprise/coderd/appearance_test.go index c83f9fb62005b..6b1c000bec91b 100644 --- a/enterprise/coderd/appearance_test.go +++ b/enterprise/coderd/appearance_test.go @@ -11,6 +11,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -30,7 +31,9 @@ func TestServiceBanners(t *testing.T) { require.False(t, sb.ServiceBanner.Enabled) coderdenttest.AddLicense(t, adminClient, coderdenttest.LicenseOptions{ - ServiceBanners: true, + Features: license.Features{ + codersdk.FeatureAppearance: 1, + }, }) // Default state diff --git a/enterprise/coderd/authorize_test.go b/enterprise/coderd/authorize_test.go index 9195387632a67..ef181e5b985aa 100644 --- a/enterprise/coderd/authorize_test.go +++ b/enterprise/coderd/authorize_test.go @@ -11,6 +11,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -28,7 +29,9 @@ func TestCheckACLPermissions(t *testing.T) { // Create adminClient, member, and org adminClient adminUser := coderdtest.CreateFirstUser(t, adminClient) _ = coderdenttest.AddLicense(t, adminClient, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) memberClient := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID) diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 2ec7548f54a49..5ad7460b1dd26 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -238,7 +238,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { api.entitlementsMu.Lock() defer api.entitlementsMu.Unlock() - entitlements, err := license.Entitlements(ctx, api.Database, api.Logger, len(api.replicaManager.All()), len(api.GitAuthConfigs), api.Keys, map[string]bool{ + entitlements, err := license.Entitlements(ctx, api.Database, api.Logger, len(api.replicaManager.All()), len(api.GitAuthConfigs), api.Keys, map[codersdk.FeatureName]bool{ codersdk.FeatureAuditLog: api.AuditLogging, codersdk.FeatureBrowserOnly: api.BrowserOnly, codersdk.FeatureSCIM: len(api.SCIMAPIKey) != 0, @@ -252,7 +252,7 @@ func (api *API) updateEntitlements(ctx context.Context) error { } entitlements.Experimental = api.DeploymentConfig.Experimental.Value - featureChanged := func(featureName string) (changed bool, enabled bool) { + featureChanged := func(featureName codersdk.FeatureName) (changed bool, enabled bool) { if api.entitlements.Features == nil { return true, entitlements.Features[featureName].Enabled } diff --git a/enterprise/coderd/coderd_test.go b/enterprise/coderd/coderd_test.go index 6d8e90c9b185d..4d67d97029830 100644 --- a/enterprise/coderd/coderd_test.go +++ b/enterprise/coderd/coderd_test.go @@ -17,6 +17,7 @@ import ( "github.com/coder/coder/enterprise/audit" "github.com/coder/coder/enterprise/coderd" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -41,10 +42,12 @@ func TestEntitlements(t *testing.T) { }) _ = coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - UserLimit: 100, - AuditLog: true, - TemplateRBAC: true, - ExternalProvisionerDaemons: true, + Features: license.Features{ + codersdk.FeatureUserLimit: 100, + codersdk.FeatureAuditLog: 1, + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, }) res, err := client.Entitlements(context.Background()) require.NoError(t, err) @@ -68,8 +71,10 @@ func TestEntitlements(t *testing.T) { }) _ = coderdtest.CreateFirstUser(t, client) license := coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - UserLimit: 100, - AuditLog: true, + Features: license.Features{ + codersdk.FeatureUserLimit: 100, + codersdk.FeatureAuditLog: 1, + }, }) res, err := client.Entitlements(context.Background()) require.NoError(t, err) @@ -99,7 +104,9 @@ func TestEntitlements(t *testing.T) { UploadedAt: database.Now(), Exp: database.Now().AddDate(1, 0, 0), JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - AuditLog: true, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + }, }), }) require.NoError(t, err) @@ -125,7 +132,9 @@ func TestEntitlements(t *testing.T) { UploadedAt: database.Now(), Exp: database.Now().AddDate(1, 0, 0), JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - AuditLog: true, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + }, }), }) require.NoError(t, err) @@ -165,7 +174,9 @@ func TestAuditLogging(t *testing.T) { }) coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - AuditLog: true, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + }, }) auditor := *api.AGPL.Auditor.Load() ea := audit.NewAuditor(audit.DefaultFilter) diff --git a/enterprise/coderd/coderdenttest/coderdenttest.go b/enterprise/coderd/coderdenttest/coderdenttest.go index fa118a204d437..0e5c8e7a1ef93 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest.go +++ b/enterprise/coderd/coderdenttest/coderdenttest.go @@ -99,21 +99,13 @@ func NewWithAPI(t *testing.T, options *Options) (*codersdk.Client, io.Closer, *c } type LicenseOptions struct { - AccountType string - AccountID string - Trial bool - AllFeatures bool - GraceAt time.Time - ExpiresAt time.Time - UserLimit int64 - AuditLog bool - BrowserOnly bool - SCIM bool - TemplateRBAC bool - HighAvailability bool - MultipleGitAuth bool - ExternalProvisionerDaemons bool - ServiceBanners bool + AccountType string + AccountID string + Trial bool + AllFeatures bool + GraceAt time.Time + ExpiresAt time.Time + Features license.Features } // AddLicense generates a new license with the options provided and inserts it. @@ -133,42 +125,6 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { if options.GraceAt.IsZero() { options.GraceAt = time.Now().Add(time.Hour) } - var auditLog int64 - if options.AuditLog { - auditLog = 1 - } - var browserOnly int64 - if options.BrowserOnly { - browserOnly = 1 - } - var scim int64 - if options.SCIM { - scim = 1 - } - highAvailability := int64(0) - if options.HighAvailability { - highAvailability = 1 - } - - rbacEnabled := int64(0) - if options.TemplateRBAC { - rbacEnabled = 1 - } - - multipleGitAuth := int64(0) - if options.MultipleGitAuth { - multipleGitAuth = 1 - } - - externalProvisionerDaemons := int64(0) - if options.ExternalProvisionerDaemons { - externalProvisionerDaemons = 1 - } - - serviceBanners := int64(0) - if options.ServiceBanners { - serviceBanners = 1 - } c := &license.Claims{ RegisteredClaims: jwt.RegisteredClaims{ @@ -183,17 +139,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string { Trial: options.Trial, Version: license.CurrentVersion, AllFeatures: options.AllFeatures, - Features: license.Features{ - UserLimit: options.UserLimit, - AuditLog: auditLog, - BrowserOnly: browserOnly, - SCIM: scim, - HighAvailability: highAvailability, - TemplateRBAC: rbacEnabled, - MultipleGitAuth: multipleGitAuth, - ExternalProvisionerDaemons: externalProvisionerDaemons, - Appearance: serviceBanners, - }, + Features: options.Features, } tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c) tok.Header[license.HeaderKeyID] = testKeyID diff --git a/enterprise/coderd/coderdenttest/coderdenttest_test.go b/enterprise/coderd/coderdenttest/coderdenttest_test.go index b6fb038d06acf..bfd5ee64152f0 100644 --- a/enterprise/coderd/coderdenttest/coderdenttest_test.go +++ b/enterprise/coderd/coderdenttest/coderdenttest_test.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/coderd/rbac" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -32,9 +33,11 @@ func TestAuthorizeAllEndpoints(t *testing.T) { }) ctx, _ := testutil.Context(t) admin := coderdtest.CreateFirstUser(t, client) - license := coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, - ExternalProvisionerDaemons: true, + lic := coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureExternalProvisionerDaemons: 1, + }, }) group, err := client.CreateGroup(ctx, admin.OrganizationID, codersdk.CreateGroupRequest{ Name: "testgroup", @@ -43,7 +46,7 @@ func TestAuthorizeAllEndpoints(t *testing.T) { groupObj := rbac.ResourceGroup.InOrg(admin.OrganizationID) a := coderdtest.NewAuthTester(ctx, t, client, api.AGPL, admin) - a.URLParams["licenses/{id}"] = fmt.Sprintf("licenses/%d", license.ID) + a.URLParams["licenses/{id}"] = fmt.Sprintf("licenses/%d", lic.ID) a.URLParams["groups/{group}"] = fmt.Sprintf("groups/%s", group.ID.String()) a.URLParams["{groupName}"] = group.Name diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 333368cb3b86a..b57f15cf3e434 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -13,6 +13,7 @@ import ( "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -26,7 +27,9 @@ func TestCreateGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -54,8 +57,10 @@ func TestCreateGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, - AuditLog: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureAuditLog: 1, + }, }) ctx, _ := testutil.Context(t) @@ -78,7 +83,9 @@ func TestCreateGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) _, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -102,7 +109,9 @@ func TestCreateGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) _, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -125,7 +134,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -157,7 +168,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -179,7 +192,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -205,7 +220,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -248,8 +265,10 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, - AuditLog: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureAuditLog: 1, + }, }) ctx, _ := testutil.Context(t) @@ -277,7 +296,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group1, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -308,7 +329,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -332,7 +355,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -356,7 +381,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) ctx, _ := testutil.Context(t) @@ -382,7 +409,9 @@ func TestPatchGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -411,7 +440,9 @@ func TestGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -431,7 +462,9 @@ func TestGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -451,7 +484,9 @@ func TestGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -481,7 +516,9 @@ func TestGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) client1, _ := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -502,7 +539,9 @@ func TestGroup(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -535,7 +574,9 @@ func TestGroup(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -576,7 +617,9 @@ func TestGroups(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) _, user3 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -622,7 +665,9 @@ func TestDeleteGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) group1, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ @@ -654,8 +699,10 @@ func TestDeleteGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, - AuditLog: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureAuditLog: 1, + }, }) ctx, _ := testutil.Context(t) @@ -681,7 +728,9 @@ func TestDeleteGroup(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) ctx, _ := testutil.Context(t) err := client.DeleteGroup(ctx, user.OrganizationID) diff --git a/enterprise/coderd/license/license.go b/enterprise/coderd/license/license.go index 47c15d7b0d1cb..4e66ab578fad6 100644 --- a/enterprise/coderd/license/license.go +++ b/enterprise/coderd/license/license.go @@ -4,7 +4,6 @@ import ( "context" "crypto/ed25519" "fmt" - "strings" "time" "github.com/golang-jwt/jwt/v4" @@ -24,12 +23,12 @@ func Entitlements( replicaCount int, gitAuthCount int, keys map[string]ed25519.PublicKey, - enablements map[string]bool, + enablements map[codersdk.FeatureName]bool, ) (codersdk.Entitlements, error) { now := time.Now() // Default all entitlements to be disabled. entitlements := codersdk.Entitlements{ - Features: map[string]codersdk.Feature{}, + Features: map[codersdk.FeatureName]codersdk.Feature{}, Warnings: []string{}, Errors: []string{}, } @@ -68,67 +67,34 @@ func Entitlements( // LicenseExpires we must be in grace period. entitlement = codersdk.EntitlementGracePeriod } - if claims.Features.UserLimit > 0 { - limit := claims.Features.UserLimit - priorLimit := entitlements.Features[codersdk.FeatureUserLimit] - if priorLimit.Limit != nil && *priorLimit.Limit > limit { - limit = *priorLimit.Limit - } - entitlements.Features[codersdk.FeatureUserLimit] = codersdk.Feature{ - Enabled: true, - Entitlement: entitlement, - Limit: &limit, - Actual: &activeUserCount, - } - } - if claims.Features.AuditLog > 0 { - entitlements.Features[codersdk.FeatureAuditLog] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: enablements[codersdk.FeatureAuditLog], - } - } - if claims.Features.BrowserOnly > 0 { - entitlements.Features[codersdk.FeatureBrowserOnly] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: enablements[codersdk.FeatureBrowserOnly], - } - } - if claims.Features.SCIM > 0 { - entitlements.Features[codersdk.FeatureSCIM] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: enablements[codersdk.FeatureSCIM], - } - } - if claims.Features.HighAvailability > 0 { - entitlements.Features[codersdk.FeatureHighAvailability] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: enablements[codersdk.FeatureHighAvailability], - } - } - if claims.Features.TemplateRBAC > 0 { - entitlements.Features[codersdk.FeatureTemplateRBAC] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: enablements[codersdk.FeatureTemplateRBAC], - } - } - if claims.Features.MultipleGitAuth > 0 { - entitlements.Features[codersdk.FeatureMultipleGitAuth] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: true, - } - } - if claims.Features.ExternalProvisionerDaemons > 0 { - entitlements.Features[codersdk.FeatureExternalProvisionerDaemons] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: true, + for featureName, featureValue := range claims.Features { + // Can this be negative? + if featureValue <= 0 { + continue } - } - if claims.Features.Appearance > 0 { - entitlements.Features[codersdk.FeatureAppearance] = codersdk.Feature{ - Entitlement: entitlement, - Enabled: true, + + switch featureName { + // User limit has special treatment as our only non-boolean feature. + case codersdk.FeatureUserLimit: + limit := featureValue + priorLimit := entitlements.Features[codersdk.FeatureUserLimit] + if priorLimit.Limit != nil && *priorLimit.Limit > limit { + limit = *priorLimit.Limit + } + entitlements.Features[codersdk.FeatureUserLimit] = codersdk.Feature{ + Enabled: true, + Entitlement: entitlement, + Limit: &limit, + Actual: &activeUserCount, + } + default: + entitlements.Features[featureName] = codersdk.Feature{ + Entitlement: entitlement, + Enabled: enablements[featureName] || featureName.AlwaysEnable(), + } } } + if claims.AllFeatures { allFeatures = true } @@ -171,7 +137,7 @@ func Entitlements( if !feature.Enabled { continue } - niceName := strings.Title(strings.ReplaceAll(featureName, "_", " ")) + niceName := featureName.Humanize() switch feature.Entitlement { case codersdk.EntitlementNotEntitled: entitlements.Warnings = append(entitlements.Warnings, @@ -249,17 +215,7 @@ var ( ErrMissingLicenseExpires = xerrors.New("license missing license_expires") ) -type Features struct { - UserLimit int64 `json:"user_limit"` - AuditLog int64 `json:"audit_log"` - BrowserOnly int64 `json:"browser_only"` - SCIM int64 `json:"scim"` - TemplateRBAC int64 `json:"template_rbac"` - HighAvailability int64 `json:"high_availability"` - MultipleGitAuth int64 `json:"multiple_git_auth"` - ExternalProvisionerDaemons int64 `json:"external_provisioner_daemons"` - Appearance int64 `json:"appearance"` -} +type Features map[codersdk.FeatureName]int64 type Claims struct { jwt.RegisteredClaims diff --git a/enterprise/coderd/license/license_test.go b/enterprise/coderd/license/license_test.go index 1dde6062e78f4..1befc39d371cc 100644 --- a/enterprise/coderd/license/license_test.go +++ b/enterprise/coderd/license/license_test.go @@ -3,7 +3,6 @@ package license_test import ( "context" "fmt" - "strings" "testing" "time" @@ -19,17 +18,13 @@ import ( func TestEntitlements(t *testing.T) { t.Parallel() - all := map[string]bool{ - codersdk.FeatureAuditLog: true, - codersdk.FeatureBrowserOnly: true, - codersdk.FeatureSCIM: true, - codersdk.FeatureHighAvailability: true, - codersdk.FeatureTemplateRBAC: true, - codersdk.FeatureMultipleGitAuth: true, - codersdk.FeatureExternalProvisionerDaemons: true, - codersdk.FeatureAppearance: true, + all := make(map[codersdk.FeatureName]bool) + for _, n := range codersdk.FeatureNames { + all[n] = true } + empty := map[codersdk.FeatureName]bool{} + t.Run("Defaults", func(t *testing.T) { t.Parallel() db := databasefake.New() @@ -49,7 +44,7 @@ func TestEntitlements(t *testing.T) { JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{}), Exp: time.Now().Add(time.Hour), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{}) + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty) require.NoError(t, err) require.True(t, entitlements.HasLicense) require.False(t, entitlements.Trial) @@ -63,19 +58,17 @@ func TestEntitlements(t *testing.T) { db := databasefake.New() db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - UserLimit: 100, - AuditLog: true, - BrowserOnly: true, - SCIM: true, - HighAvailability: true, - TemplateRBAC: true, - MultipleGitAuth: true, - ExternalProvisionerDaemons: true, - ServiceBanners: true, + Features: func() license.Features { + f := make(license.Features) + for _, name := range codersdk.FeatureNames { + f[name] = 1 + } + return f + }(), }), Exp: time.Now().Add(time.Hour), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{}) + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty) require.NoError(t, err) require.True(t, entitlements.HasLicense) require.False(t, entitlements.Trial) @@ -88,16 +81,13 @@ func TestEntitlements(t *testing.T) { db := databasefake.New() db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - UserLimit: 100, - AuditLog: true, - BrowserOnly: true, - SCIM: true, - HighAvailability: true, - TemplateRBAC: true, - ExternalProvisionerDaemons: true, - ServiceBanners: true, - GraceAt: time.Now().Add(-time.Hour), - ExpiresAt: time.Now().Add(time.Hour), + Features: license.Features{ + codersdk.FeatureUserLimit: 100, + codersdk.FeatureAuditLog: 1, + }, + + GraceAt: time.Now().Add(-time.Hour), + ExpiresAt: time.Now().Add(time.Hour), }), Exp: time.Now().Add(time.Hour), }) @@ -105,20 +95,12 @@ func TestEntitlements(t *testing.T) { require.NoError(t, err) require.True(t, entitlements.HasLicense) require.False(t, entitlements.Trial) - for _, featureName := range codersdk.FeatureNames { - if featureName == codersdk.FeatureUserLimit { - continue - } - if featureName == codersdk.FeatureHighAvailability { - continue - } - if featureName == codersdk.FeatureMultipleGitAuth { - continue - } - niceName := strings.Title(strings.ReplaceAll(featureName, "_", " ")) - require.Equal(t, codersdk.EntitlementGracePeriod, entitlements.Features[featureName].Entitlement) - require.Contains(t, entitlements.Warnings, fmt.Sprintf("%s is enabled but your license for this feature is expired.", niceName)) - } + + require.Equal(t, codersdk.EntitlementGracePeriod, entitlements.Features[codersdk.FeatureAuditLog].Entitlement) + require.Contains( + t, entitlements.Warnings, + fmt.Sprintf("%s is enabled but your license for this feature is expired.", codersdk.FeatureAuditLog.Humanize()), + ) }) t.Run("SingleLicenseNotEntitled", func(t *testing.T) { t.Parallel() @@ -141,7 +123,7 @@ func TestEntitlements(t *testing.T) { if featureName == codersdk.FeatureMultipleGitAuth { continue } - niceName := strings.Title(strings.ReplaceAll(featureName, "_", " ")) + niceName := featureName.Humanize() // Ensures features that are not entitled are properly disabled. require.False(t, entitlements.Features[featureName].Enabled) require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement) @@ -159,11 +141,13 @@ func TestEntitlements(t *testing.T) { }) db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - UserLimit: 1, + Features: license.Features{ + codersdk.FeatureUserLimit: 1, + }, }), Exp: time.Now().Add(time.Hour), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{}) + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty) require.NoError(t, err) require.True(t, entitlements.HasLicense) require.Contains(t, entitlements.Warnings, "Your deployment has 2 active users but is only licensed for 1.") @@ -175,17 +159,21 @@ func TestEntitlements(t *testing.T) { db.InsertUser(context.Background(), database.InsertUserParams{}) db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - UserLimit: 10, + Features: license.Features{ + codersdk.FeatureUserLimit: 10, + }, }), Exp: time.Now().Add(time.Hour), }) db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - UserLimit: 1, + Features: license.Features{ + codersdk.FeatureUserLimit: 1, + }, }), Exp: time.Now().Add(time.Hour), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{}) + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty) require.NoError(t, err) require.True(t, entitlements.HasLicense) require.Empty(t, entitlements.Warnings) @@ -208,7 +196,7 @@ func TestEntitlements(t *testing.T) { }), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{}) + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty) require.NoError(t, err) require.True(t, entitlements.HasLicense) require.False(t, entitlements.Trial) @@ -252,10 +240,12 @@ func TestEntitlements(t *testing.T) { db.InsertLicense(context.Background(), database.InsertLicenseParams{ Exp: time.Now().Add(time.Hour), JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - AuditLog: true, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + }, }), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, 1, coderdenttest.Keys, map[string]bool{ + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, 1, coderdenttest.Keys, map[codersdk.FeatureName]bool{ codersdk.FeatureHighAvailability: true, }) require.NoError(t, err) @@ -269,13 +259,15 @@ func TestEntitlements(t *testing.T) { db := databasefake.New() db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - HighAvailability: true, - GraceAt: time.Now().Add(-time.Hour), - ExpiresAt: time.Now().Add(time.Hour), + Features: license.Features{ + codersdk.FeatureHighAvailability: 1, + }, + GraceAt: time.Now().Add(-time.Hour), + ExpiresAt: time.Now().Add(time.Hour), }), Exp: time.Now().Add(time.Hour), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, 1, coderdenttest.Keys, map[string]bool{ + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, 1, coderdenttest.Keys, map[codersdk.FeatureName]bool{ codersdk.FeatureHighAvailability: true, }) require.NoError(t, err) @@ -300,10 +292,12 @@ func TestEntitlements(t *testing.T) { db.InsertLicense(context.Background(), database.InsertLicenseParams{ Exp: time.Now().Add(time.Hour), JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - AuditLog: true, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + }, }), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 2, coderdenttest.Keys, map[string]bool{ + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 2, coderdenttest.Keys, map[codersdk.FeatureName]bool{ codersdk.FeatureMultipleGitAuth: true, }) require.NoError(t, err) @@ -317,13 +311,15 @@ func TestEntitlements(t *testing.T) { db := databasefake.New() db.InsertLicense(context.Background(), database.InsertLicenseParams{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ - MultipleGitAuth: true, - GraceAt: time.Now().Add(-time.Hour), - ExpiresAt: time.Now().Add(time.Hour), + GraceAt: time.Now().Add(-time.Hour), + ExpiresAt: time.Now().Add(time.Hour), + Features: license.Features{ + codersdk.FeatureMultipleGitAuth: 1, + }, }), Exp: time.Now().Add(time.Hour), }) - entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 2, coderdenttest.Keys, map[string]bool{ + entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 2, coderdenttest.Keys, map[codersdk.FeatureName]bool{ codersdk.FeatureMultipleGitAuth: true, }) require.NoError(t, err) diff --git a/enterprise/coderd/licenses_test.go b/enterprise/coderd/licenses_test.go index 34317773d539f..4c0595cc12fa8 100644 --- a/enterprise/coderd/licenses_test.go +++ b/enterprise/coderd/licenses_test.go @@ -2,7 +2,6 @@ package coderd_test import ( "context" - "encoding/json" "net/http" "testing" @@ -27,14 +26,16 @@ func TestPostLicense(t *testing.T) { respLic := coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountType: license.AccountTypeSalesforce, AccountID: "testing", - AuditLog: true, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + }, }) assert.GreaterOrEqual(t, respLic.ID, int32(0)) // just a couple spot checks for sanity assert.Equal(t, "testing", respLic.Claims["account_id"]) - features, ok := respLic.Claims["features"].(map[string]interface{}) - require.True(t, ok) - assert.Equal(t, json.Number("1"), features[codersdk.FeatureAuditLog]) + features, err := respLic.Features() + require.NoError(t, err) + assert.EqualValues(t, 1, features[codersdk.FeatureAuditLog]) }) t.Run("Unauthorized", func(t *testing.T) { @@ -78,21 +79,24 @@ func TestGetLicense(t *testing.T) { defer cancel() coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - AccountID: "testing", - AuditLog: true, - SCIM: true, - BrowserOnly: true, - TemplateRBAC: true, + AccountID: "testing", + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + codersdk.FeatureSCIM: 1, + codersdk.FeatureBrowserOnly: 1, + codersdk.FeatureTemplateRBAC: 1, + }, }) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - AccountID: "testing2", - AuditLog: true, - SCIM: true, - BrowserOnly: true, - Trial: true, - UserLimit: 200, - TemplateRBAC: false, + AccountID: "testing2", + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + codersdk.FeatureSCIM: 1, + codersdk.FeatureBrowserOnly: 1, + codersdk.FeatureUserLimit: 200, + }, + Trial: true, }) licenses, err := client.Licenses(ctx) @@ -100,31 +104,27 @@ func TestGetLicense(t *testing.T) { require.Len(t, licenses, 2) assert.Equal(t, int32(1), licenses[0].ID) assert.Equal(t, "testing", licenses[0].Claims["account_id"]) - assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("0"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), - codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureHighAvailability: json.Number("0"), - codersdk.FeatureTemplateRBAC: json.Number("1"), - codersdk.FeatureMultipleGitAuth: json.Number("0"), - codersdk.FeatureExternalProvisionerDaemons: json.Number("0"), - codersdk.FeatureAppearance: json.Number("0"), - }, licenses[0].Claims["features"]) + + features, err := licenses[0].Features() + require.NoError(t, err) + assert.Equal(t, map[codersdk.FeatureName]int64{ + codersdk.FeatureAuditLog: 1, + codersdk.FeatureSCIM: 1, + codersdk.FeatureBrowserOnly: 1, + codersdk.FeatureTemplateRBAC: 1, + }, features) assert.Equal(t, int32(2), licenses[1].ID) assert.Equal(t, "testing2", licenses[1].Claims["account_id"]) assert.Equal(t, true, licenses[1].Claims["trial"]) - assert.Equal(t, map[string]interface{}{ - codersdk.FeatureUserLimit: json.Number("200"), - codersdk.FeatureAuditLog: json.Number("1"), - codersdk.FeatureSCIM: json.Number("1"), - codersdk.FeatureBrowserOnly: json.Number("1"), - codersdk.FeatureHighAvailability: json.Number("0"), - codersdk.FeatureTemplateRBAC: json.Number("0"), - codersdk.FeatureMultipleGitAuth: json.Number("0"), - codersdk.FeatureExternalProvisionerDaemons: json.Number("0"), - codersdk.FeatureAppearance: json.Number("0"), - }, licenses[1].Claims["features"]) + + features, err = licenses[1].Features() + require.NoError(t, err) + assert.Equal(t, map[codersdk.FeatureName]int64{ + codersdk.FeatureUserLimit: 200, + codersdk.FeatureAuditLog: 1, + codersdk.FeatureSCIM: 1, + codersdk.FeatureBrowserOnly: 1, + }, features) }) } @@ -168,12 +168,16 @@ func TestDeleteLicense(t *testing.T) { coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "testing", - AuditLog: true, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + }, }) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "testing2", - AuditLog: true, - UserLimit: 200, + Features: license.Features{ + codersdk.FeatureAuditLog: 1, + codersdk.FeatureUserLimit: 200, + }, }) licenses, err := client.Licenses(ctx) diff --git a/enterprise/coderd/provisionerdaemons_test.go b/enterprise/coderd/provisionerdaemons_test.go index f603f3569e807..65c5eaebb26ca 100644 --- a/enterprise/coderd/provisionerdaemons_test.go +++ b/enterprise/coderd/provisionerdaemons_test.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/coderd/provisionerdserver" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" ) @@ -36,7 +37,9 @@ func TestProvisionerDaemonServe(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - ExternalProvisionerDaemons: true, + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, }) srv, err := client.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{ codersdk.ProvisionerTypeEcho, @@ -50,7 +53,9 @@ func TestProvisionerDaemonServe(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - ExternalProvisionerDaemons: true, + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, }) another := coderdtest.CreateAnotherUser(t, client, user.OrganizationID) _, err := another.ServeProvisionerDaemon(context.Background(), user.OrganizationID, []codersdk.ProvisionerType{ @@ -69,7 +74,9 @@ func TestProvisionerDaemonServe(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - ExternalProvisionerDaemons: true, + Features: license.Features{ + codersdk.FeatureExternalProvisionerDaemons: 1, + }, }) closer := coderdtest.NewExternalProvisionerDaemon(t, client, user.OrganizationID, map[string]string{ provisionerdserver.TagScope: provisionerdserver.ScopeUser, diff --git a/enterprise/coderd/replicas_test.go b/enterprise/coderd/replicas_test.go index 4713c276adaf1..4e910ac84a56c 100644 --- a/enterprise/coderd/replicas_test.go +++ b/enterprise/coderd/replicas_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/coder/coderd/database/dbtestutil" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -58,7 +59,9 @@ func TestReplicas(t *testing.T) { }) firstUser := coderdtest.CreateFirstUser(t, firstClient) coderdenttest.AddLicense(t, firstClient, coderdenttest.LicenseOptions{ - HighAvailability: true, + Features: license.Features{ + codersdk.FeatureHighAvailability: 1, + }, }) secondClient := coderdenttest.New(t, &coderdenttest.Options{ @@ -100,7 +103,9 @@ func TestReplicas(t *testing.T) { }) firstUser := coderdtest.CreateFirstUser(t, firstClient) coderdenttest.AddLicense(t, firstClient, coderdenttest.LicenseOptions{ - HighAvailability: true, + Features: license.Features{ + codersdk.FeatureHighAvailability: 1, + }, }) secondClient := coderdenttest.New(t, &coderdenttest.Options{ diff --git a/enterprise/coderd/scim_test.go b/enterprise/coderd/scim_test.go index c0ebf1356d120..491b4f8180aa8 100644 --- a/enterprise/coderd/scim_test.go +++ b/enterprise/coderd/scim_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/cryptorand" "github.com/coder/coder/enterprise/coderd" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -66,7 +67,9 @@ func TestScim(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "coolin", - SCIM: false, + Features: license.Features{ + codersdk.FeatureSCIM: 0, + }, }) res, err := client.Request(ctx, "POST", "/scim/v2/Users", struct{}{}) @@ -85,7 +88,9 @@ func TestScim(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "coolin", - SCIM: true, + Features: license.Features{ + codersdk.FeatureSCIM: 1, + }, }) res, err := client.Request(ctx, "POST", "/scim/v2/Users", struct{}{}) @@ -105,7 +110,9 @@ func TestScim(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "coolin", - SCIM: true, + Features: license.Features{ + codersdk.FeatureSCIM: 1, + }, }) sUser := makeScimUser(t) @@ -136,7 +143,9 @@ func TestScim(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "coolin", - SCIM: false, + Features: license.Features{ + codersdk.FeatureSCIM: 0, + }, }) res, err := client.Request(ctx, "PATCH", "/scim/v2/Users/bob", struct{}{}) @@ -155,7 +164,9 @@ func TestScim(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "coolin", - SCIM: true, + Features: license.Features{ + codersdk.FeatureSCIM: 1, + }, }) res, err := client.Request(ctx, "PATCH", "/scim/v2/Users/bob", struct{}{}) @@ -175,7 +186,9 @@ func TestScim(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ AccountID: "coolin", - SCIM: true, + Features: license.Features{ + codersdk.FeatureSCIM: 1, + }, }) sUser := makeScimUser(t) diff --git a/enterprise/coderd/templates_test.go b/enterprise/coderd/templates_test.go index f257766b0bddb..725c6cc5eb621 100644 --- a/enterprise/coderd/templates_test.go +++ b/enterprise/coderd/templates_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/codersdk" "github.com/coder/coder/cryptorand" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/testutil" ) @@ -27,7 +28,9 @@ func TestTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -68,7 +71,9 @@ func TestTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -92,7 +97,9 @@ func TestTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) client1, _ := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -142,7 +149,9 @@ func TestTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -180,7 +189,9 @@ func TestTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -218,7 +229,9 @@ func TestTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) @@ -266,7 +279,9 @@ func TestTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) client1, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -322,7 +337,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -374,8 +391,10 @@ func TestUpdateTemplateACL(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, - AuditLog: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + codersdk.FeatureAuditLog: 1, + }, }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) @@ -405,7 +424,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -466,7 +487,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) @@ -491,7 +514,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) @@ -516,7 +541,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) _, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -542,7 +569,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) client2, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -577,7 +606,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) client2, user2 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -619,7 +650,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) @@ -641,7 +674,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) client1, user1 := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -706,7 +741,9 @@ func TestUpdateTemplateACL(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) client1, _ := coderdtest.CreateAnotherUserWithUser(t, client, user.OrganizationID) @@ -764,7 +801,9 @@ func TestTemplateAccess(t *testing.T) { ownerClient := coderdenttest.New(t, nil) owner := coderdtest.CreateFirstUser(t, ownerClient) _ = coderdenttest.AddLicense(t, ownerClient, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) type coderUser struct { diff --git a/enterprise/coderd/workspaceagents_test.go b/enterprise/coderd/workspaceagents_test.go index b103e8e2e4e41..aaef3c28f999a 100644 --- a/enterprise/coderd/workspaceagents_test.go +++ b/enterprise/coderd/workspaceagents_test.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -39,7 +40,9 @@ func TestBlockNonBrowser(t *testing.T) { }) user := coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - BrowserOnly: true, + Features: license.Features{ + codersdk.FeatureBrowserOnly: 1, + }, }) _, agent := setupWorkspaceAgent(t, client, user, 0) _, err := client.DialWorkspaceAgent(context.Background(), agent.ID, nil) @@ -56,7 +59,9 @@ func TestBlockNonBrowser(t *testing.T) { }) user := coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - BrowserOnly: false, + Features: license.Features{ + codersdk.FeatureBrowserOnly: 0, + }, }) _, agent := setupWorkspaceAgent(t, client, user, 0) conn, err := client.DialWorkspaceAgent(context.Background(), agent.ID, nil) diff --git a/enterprise/coderd/workspacequota_test.go b/enterprise/coderd/workspacequota_test.go index 98118f310aa7f..ed4b5448fe0a4 100644 --- a/enterprise/coderd/workspacequota_test.go +++ b/enterprise/coderd/workspacequota_test.go @@ -10,6 +10,7 @@ import ( "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" "github.com/coder/coder/testutil" @@ -45,7 +46,9 @@ func TestWorkspaceQuota(t *testing.T) { user := coderdtest.CreateFirstUser(t, client) coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) verifyQuota(ctx, t, client, 0, 0) diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 824b3febb191c..ef14d8a6be628 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -12,6 +12,7 @@ import ( "github.com/coder/coder/coderd/util/ptr" "github.com/coder/coder/codersdk" "github.com/coder/coder/enterprise/coderd/coderdenttest" + "github.com/coder/coder/enterprise/coderd/license" "github.com/coder/coder/testutil" ) @@ -26,7 +27,9 @@ func TestCreateWorkspace(t *testing.T) { client := coderdenttest.New(t, nil) user := coderdtest.CreateFirstUser(t, client) _ = coderdenttest.AddLicense(t, client, coderdenttest.LicenseOptions{ - TemplateRBAC: true, + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, }) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go index 5351084a8c041..1d068795c0eb4 100644 --- a/scripts/apitypings/main.go +++ b/scripts/apitypings/main.go @@ -63,7 +63,11 @@ type TypescriptTypes struct { // String just combines all the codeblocks. func (t TypescriptTypes) String() string { var s strings.Builder - _, _ = s.WriteString("// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.\n\n") + const prelude = ` +// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT. + +` + _, _ = s.WriteString(prelude) sortedTypes := make([]string, 0, len(t.Types)) sortedEnums := make([]string, 0, len(t.Enums)) @@ -223,6 +227,18 @@ func (g *Generator) generateAll() (*TypescriptTypes, error) { name, strings.Join(values, " | "), )) + var pluralName string + if strings.HasSuffix(name, "s") { + pluralName = name + "es" + } else { + pluralName = name + "s" + } + + // Generate array used for enumerating all possible values. + _, _ = s.WriteString(fmt.Sprintf("export const %s: %s[] = [%s]\n", + pluralName, name, strings.Join(values, ", "), + )) + enumCodeBlocks[name] = s.String() } @@ -644,6 +660,7 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) { aboveTypeLine = aboveTypeLine + "\n" } aboveTypeLine = aboveTypeLine + valueType.AboveTypeLine + return TypescriptType{ ValueType: fmt.Sprintf("Record<%s, %s>", keyType.ValueType, valueType.ValueType), AboveTypeLine: aboveTypeLine, diff --git a/scripts/apitypings/testdata/enums/enums.ts b/scripts/apitypings/testdata/enums/enums.ts index 0aca42a2feebd..21b6f1934e499 100644 --- a/scripts/apitypings/testdata/enums/enums.ts +++ b/scripts/apitypings/testdata/enums/enums.ts @@ -2,3 +2,4 @@ // From codersdk/enums.go export type Enum = "bar" | "baz" | "foo" | "qux" +export const Enums: Enum[] = ["bar", "baz", "foo", "qux"] diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 3c7ce3c828d00..7342994685944 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -1,5 +1,4 @@ import { useSelector } from "@xstate/react" -import { FeatureNames } from "api/types" import { FullScreenLoader } from "components/Loader/FullScreenLoader" import { RequirePermission } from "components/RequirePermission/RequirePermission" import { TemplateLayout } from "components/TemplateLayout/TemplateLayout" @@ -196,7 +195,7 @@ export const AppRouter: FC = () => { element={ diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5b32f38b572b5..fb0cac686b734 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -16,17 +16,28 @@ export const hardCodedCSRFCookie = (): string => { return csrfToken } -// defaultEntitlements has a default set of disabled functionality. -export const defaultEntitlements = (): TypesGen.Entitlements => { - const features: TypesGen.Entitlements["features"] = {} - for (const feature in Types.FeatureNames) { - features[feature] = { +// withDefaultFeatures sets all unspecified features to not_entitled and disabled. +export const withDefaultFeatures = ( + fs: Partial, +): TypesGen.Entitlements["features"] => { + for (const k in TypesGen.FeatureNames) { + const feature = k as TypesGen.FeatureName + // Skip fields that are already filled. + if (fs[feature] !== undefined) { + continue + } + fs[feature] = { enabled: false, entitlement: "not_entitled", } } + return fs as TypesGen.Entitlements["features"] +} + +// defaultEntitlements has a default set of disabled functionality. +export const defaultEntitlements = (): TypesGen.Entitlements => { return { - features: features, + features: withDefaultFeatures({}), has_license: false, errors: [], warnings: [], diff --git a/site/src/api/types.ts b/site/src/api/types.ts index cc8103a080e6b..daf4e451ac5e8 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -14,14 +14,3 @@ export interface ReconnectingPTYRequest { export type WorkspaceBuildTransition = "start" | "stop" | "delete" export type Message = { message: string } - -// Keep up to date with coder/codersdk/features.go -export enum FeatureNames { - AuditLog = "audit_log", - UserLimit = "user_limit", - BrowserOnly = "browser_only", - SCIM = "scim", - TemplateRBAC = "template_rbac", - HighAvailability = "high_availability", - Appearance = "appearance", -} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7570a7d9289de..665e6517dacde 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -331,7 +331,7 @@ export interface DeploymentConfigField { // From codersdk/features.go export interface Entitlements { - readonly features: Record + readonly features: Record readonly warnings: string[] readonly errors: string[] readonly has_license: boolean @@ -1020,42 +1020,99 @@ export interface WorkspacesResponse { // From codersdk/apikey.go export type APIKeyScope = "all" | "application_connect" +export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"] // From codersdk/audit.go export type AuditAction = "create" | "delete" | "start" | "stop" | "write" +export const AuditActions: AuditAction[] = [ + "create", + "delete", + "start", + "stop", + "write", +] // From codersdk/workspacebuilds.go export type BuildReason = "autostart" | "autostop" | "initiator" +export const BuildReasons: BuildReason[] = [ + "autostart", + "autostop", + "initiator", +] // From codersdk/features.go export type Entitlement = "entitled" | "grace_period" | "not_entitled" +export const Entitlements: Entitlement[] = [ + "entitled", + "grace_period", + "not_entitled", +] + +// From codersdk/features.go +export type FeatureName = + | "appearance" + | "audit_log" + | "browser_only" + | "external_provisioner_daemons" + | "high_availability" + | "multiple_git_auth" + | "scim" + | "template_rbac" + | "user_limit" +export const FeatureNames: FeatureName[] = [ + "appearance", + "audit_log", + "browser_only", + "external_provisioner_daemons", + "high_availability", + "multiple_git_auth", + "scim", + "template_rbac", + "user_limit", +] // From codersdk/agentconn.go export type ListeningPortNetwork = "tcp" +export const ListeningPortNetworks: ListeningPortNetwork[] = ["tcp"] // From codersdk/provisionerdaemons.go export type LogLevel = "debug" | "error" | "info" | "trace" | "warn" +export const LogLevels: LogLevel[] = ["debug", "error", "info", "trace", "warn"] // From codersdk/provisionerdaemons.go export type LogSource = "provisioner" | "provisioner_daemon" +export const LogSources: LogSource[] = ["provisioner", "provisioner_daemon"] // From codersdk/apikey.go export type LoginType = "github" | "oidc" | "password" | "token" +export const LoginTypes: LoginType[] = ["github", "oidc", "password", "token"] // From codersdk/parameters.go export type ParameterDestinationScheme = | "environment_variable" | "none" | "provisioner_variable" +export const ParameterDestinationSchemes: ParameterDestinationScheme[] = [ + "environment_variable", + "none", + "provisioner_variable", +] // From codersdk/parameters.go export type ParameterScope = "import_job" | "template" | "workspace" +export const ParameterScopes: ParameterScope[] = [ + "import_job", + "template", + "workspace", +] // From codersdk/parameters.go export type ParameterSourceScheme = "data" | "none" +export const ParameterSourceSchemes: ParameterSourceScheme[] = ["data", "none"] // From codersdk/parameters.go export type ParameterTypeSystem = "hcl" | "none" +export const ParameterTypeSystems: ParameterTypeSystem[] = ["hcl", "none"] // From codersdk/provisionerdaemons.go export type ProvisionerJobStatus = @@ -1065,12 +1122,22 @@ export type ProvisionerJobStatus = | "pending" | "running" | "succeeded" +export const ProvisionerJobStatuses: ProvisionerJobStatus[] = [ + "canceled", + "canceling", + "failed", + "pending", + "running", + "succeeded", +] // From codersdk/organizations.go export type ProvisionerStorageMethod = "file" +export const ProvisionerStorageMethods: ProvisionerStorageMethod[] = ["file"] // From codersdk/organizations.go export type ProvisionerType = "echo" | "terraform" +export const ProvisionerTypes: ProvisionerType[] = ["echo", "terraform"] // From codersdk/audit.go export type ResourceType = @@ -1083,15 +1150,33 @@ export type ResourceType = | "user" | "workspace" | "workspace_build" +export const ResourceTypes: ResourceType[] = [ + "api_key", + "git_ssh_key", + "group", + "organization", + "template", + "template_version", + "user", + "workspace", + "workspace_build", +] // From codersdk/sse.go export type ServerSentEventType = "data" | "error" | "ping" +export const ServerSentEventTypes: ServerSentEventType[] = [ + "data", + "error", + "ping", +] // From codersdk/templates.go export type TemplateRole = "" | "admin" | "use" +export const TemplateRoles: TemplateRole[] = ["", "admin", "use"] // From codersdk/users.go export type UserStatus = "active" | "suspended" +export const UserStatuses: UserStatus[] = ["active", "suspended"] // From codersdk/workspaceagents.go export type WorkspaceAgentStatus = @@ -1099,6 +1184,12 @@ export type WorkspaceAgentStatus = | "connecting" | "disconnected" | "timeout" +export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = [ + "connected", + "connecting", + "disconnected", + "timeout", +] // From codersdk/workspaceapps.go export type WorkspaceAppHealth = @@ -1106,9 +1197,20 @@ export type WorkspaceAppHealth = | "healthy" | "initializing" | "unhealthy" +export const WorkspaceAppHealths: WorkspaceAppHealth[] = [ + "disabled", + "healthy", + "initializing", + "unhealthy", +] // From codersdk/workspaceapps.go export type WorkspaceAppSharingLevel = "authenticated" | "owner" | "public" +export const WorkspaceAppSharingLevels: WorkspaceAppSharingLevel[] = [ + "authenticated", + "owner", + "public", +] // From codersdk/workspacebuilds.go export type WorkspaceStatus = @@ -1122,9 +1224,26 @@ export type WorkspaceStatus = | "starting" | "stopped" | "stopping" +export const WorkspaceStatuses: WorkspaceStatus[] = [ + "canceled", + "canceling", + "deleted", + "deleting", + "failed", + "pending", + "running", + "starting", + "stopped", + "stopping", +] // From codersdk/workspacebuilds.go export type WorkspaceTransition = "delete" | "start" | "stop" +export const WorkspaceTransitions: WorkspaceTransition[] = [ + "delete", + "start", + "stop", +] // From codersdk/deploymentconfig.go export type Flaggable = string | number | boolean | string[] | GitAuthConfig[] diff --git a/site/src/components/Navbar/Navbar.tsx b/site/src/components/Navbar/Navbar.tsx index 811410e2b21cb..7941a95b1b92a 100644 --- a/site/src/components/Navbar/Navbar.tsx +++ b/site/src/components/Navbar/Navbar.tsx @@ -1,5 +1,4 @@ import { shallowEqual, useActor, useSelector } from "@xstate/react" -import { FeatureNames } from "api/types" import { useContext, FC } from "react" import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { XServiceContext } from "../../xServices/StateContext" @@ -17,8 +16,7 @@ export const Navbar: FC = () => { shallowEqual, ) const canViewAuditLog = - featureVisibility[FeatureNames.AuditLog] && - Boolean(permissions?.viewAuditLog) + featureVisibility["audit_log"] && Boolean(permissions?.viewAuditLog) const canViewDeployment = Boolean(permissions?.viewDeploymentConfig) const onSignOut = () => authSend("SIGN_OUT") diff --git a/site/src/hooks/useFeatureVisibility.ts b/site/src/hooks/useFeatureVisibility.ts index 715dbe76948e2..7e0a860972c58 100644 --- a/site/src/hooks/useFeatureVisibility.ts +++ b/site/src/hooks/useFeatureVisibility.ts @@ -1,10 +1,10 @@ import { useSelector } from "@xstate/react" -import { FeatureNames } from "api/types" +import { FeatureName } from "api/typesGenerated" import { useContext } from "react" import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { XServiceContext } from "xServices/StateContext" -export const useFeatureVisibility = (): Record => { +export const useFeatureVisibility = (): Record => { const xServices = useContext(XServiceContext) return useSelector(xServices.entitlementsXService, selectFeatureVisibility) } diff --git a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx index ee1e1809d9a2c..6c0b16cddf33b 100644 --- a/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPage.tsx @@ -1,5 +1,4 @@ import { useActor } from "@xstate/react" -import { FeatureNames } from "api/types" import { AppearanceConfig } from "api/typesGenerated" import { useContext, FC } from "react" import { Helmet } from "react-helmet-async" @@ -20,7 +19,7 @@ const AppearanceSettingsPage: FC = () => { const appearance = appearanceXService.context.appearance const isEntitled = - entitlementsState.context.entitlements.features[FeatureNames.Appearance] + entitlementsState.context.entitlements.features["appearance"] .entitlement !== "not_entitled" const updateAppearance = ( diff --git a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx index 82f0b0c1cbedb..77300d16fac72 100644 --- a/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/SecuritySettingsPage/SecuritySettingsPage.tsx @@ -1,5 +1,4 @@ import { useActor } from "@xstate/react" -import { FeatureNames } from "api/types" import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout" import { useContext, FC } from "react" import { Helmet } from "react-helmet-async" @@ -21,13 +20,11 @@ const SecuritySettingsPage: FC = () => { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 7f801b17e8df8..3c8f6324ac35b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,5 +1,4 @@ import { useActor, useSelector } from "@xstate/react" -import { FeatureNames } from "api/types" import dayjs from "dayjs" import { useContext, useEffect } from "react" import { Helmet } from "react-helmet-async" @@ -123,9 +122,9 @@ export const WorkspaceReadyPage = ({ resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} - hideSSHButton={featureVisibility[FeatureNames.BrowserOnly]} + hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={ - !experimental || featureVisibility[FeatureNames.BrowserOnly] + !experimental || featureVisibility["browser_only"] } workspaceErrors={{ [WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning, diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index ffcd3ea8b8b35..18bfbb0ccba5e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1,3 +1,4 @@ +import { withDefaultFeatures } from "./../api/api" import { FieldError } from "api/errors" import { everyOneGroup } from "util/groups" import * as Types from "../api/types" @@ -938,7 +939,7 @@ export const MockEntitlements: TypesGen.Entitlements = { errors: [], warnings: [], has_license: false, - features: {}, + features: withDefaultFeatures({}), experimental: false, trial: false, } @@ -949,7 +950,7 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { has_license: true, experimental: false, trial: false, - features: { + features: withDefaultFeatures({ user_limit: { enabled: true, entitlement: "grace_period", @@ -964,7 +965,7 @@ export const MockEntitlementsWithWarnings: TypesGen.Entitlements = { enabled: true, entitlement: "entitled", }, - }, + }), } export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { @@ -973,12 +974,12 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { has_license: true, experimental: false, trial: false, - features: { + features: withDefaultFeatures({ audit_log: { enabled: true, entitlement: "entitled", }, - }, + }), } export const MockAuditLog: TypesGen.AuditLog = { diff --git a/site/src/xServices/entitlements/entitlementsSelectors.ts b/site/src/xServices/entitlements/entitlementsSelectors.ts index 780eb47c0a191..b634a2a25ed0c 100644 --- a/site/src/xServices/entitlements/entitlementsSelectors.ts +++ b/site/src/xServices/entitlements/entitlementsSelectors.ts @@ -1,4 +1,4 @@ -import { Feature } from "api/typesGenerated" +import { Feature, FeatureName } from "api/typesGenerated" import { State } from "xstate" import { EntitlementsContext, EntitlementsEvent } from "./entitlementsXService" @@ -28,7 +28,7 @@ export const getFeatureVisibility = ( export const selectFeatureVisibility = ( state: EntitlementState, -): Record => { +): Record => { return getFeatureVisibility( state.context.entitlements.has_license, state.context.entitlements.features, diff --git a/site/src/xServices/entitlements/entitlementsXService.ts b/site/src/xServices/entitlements/entitlementsXService.ts index a1e8bb0d9b895..e7e9b78ddd3fc 100644 --- a/site/src/xServices/entitlements/entitlementsXService.ts +++ b/site/src/xServices/entitlements/entitlementsXService.ts @@ -1,3 +1,4 @@ +import { withDefaultFeatures } from "./../../api/api" import { MockEntitlementsWithWarnings } from "testHelpers/entities" import { assign, createMachine } from "xstate" import * as API from "../../api/api" @@ -22,7 +23,7 @@ export type EntitlementsEvent = const emptyEntitlements = { errors: [], warnings: [], - features: {}, + features: withDefaultFeatures({}), has_license: false, experimental: false, trial: false,