refactor: deduplicate / type license feature code (#5734)

This commit is contained in:
Ammar Bandukwala 2023-01-17 17:04:29 -06:00 committed by GitHub
parent ea1b03f7c9
commit 501cfa9e8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 648 additions and 396 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
for featureName, featureValue := range claims.Features {
// Can this be negative?
if featureValue <= 0 {
continue
}
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,
}
}
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,3 +2,4 @@
// From codersdk/enums.go
export type Enum = "bar" | "baz" | "foo" | "qux"
export const Enums: Enum[] = ["bar", "baz", "foo", "qux"]

View File

@ -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"
@ -195,7 +194,7 @@ export const AppRouter: FC = () => {
element={
<RequirePermission
isFeatureVisible={
featureVisibility[FeatureNames.AuditLog] &&
featureVisibility["audit_log"] &&
Boolean(permissions?.viewAuditLog)
}
>

View File

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

View File

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

View File

@ -333,7 +333,7 @@ export interface DeploymentConfigField<T extends Flaggable> {
// From codersdk/features.go
export interface Entitlements {
readonly features: Record<string, Feature>
readonly features: Record<FeatureName, Feature>
readonly warnings: string[]
readonly errors: string[]
readonly has_license: boolean
@ -1051,42 +1051,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 =
@ -1096,12 +1153,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 =
@ -1114,15 +1181,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 =
@ -1130,6 +1215,12 @@ export type WorkspaceAgentStatus =
| "connecting"
| "disconnected"
| "timeout"
export const WorkspaceAgentStatuses: WorkspaceAgentStatus[] = [
"connected",
"connecting",
"disconnected",
"timeout",
]
// From codersdk/workspaceapps.go
export type WorkspaceAppHealth =
@ -1137,9 +1228,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 =
@ -1153,9 +1255,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[]

View File

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

View File

@ -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<FeatureNames, boolean> => {
export const useFeatureVisibility = (): Record<FeatureName, boolean> => {
const xServices = useContext(XServiceContext)
return useSelector(xServices.entitlementsXService, selectFeatureVisibility)
}

View File

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

View File

@ -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 = () => {
<SecuritySettingsPageView
deploymentConfig={deploymentConfig}
featureAuditLogEnabled={
entitlementsState.context.entitlements.features[FeatureNames.AuditLog]
.enabled
entitlementsState.context.entitlements.features["audit_log"].enabled
}
featureBrowserOnlyEnabled={
entitlementsState.context.entitlements.features[
FeatureNames.BrowserOnly
].enabled
entitlementsState.context.entitlements.features["browser_only"]
.enabled
}
/>
</>

View File

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

View File

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

View File

@ -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<string, boolean> => {
): Record<FeatureName, boolean> => {
return getFeatureVisibility(
state.context.entitlements.has_license,
state.context.entitlements.features,

View File

@ -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"
@ -18,7 +19,7 @@ export type EntitlementsEvent =
const emptyEntitlements = {
errors: [],
warnings: [],
features: {},
features: withDefaultFeatures({}),
has_license: false,
experimental: false,
trial: false,