Files
coder/enterprise/coderd/license/license_test.go
T
Ethan 08e17a07fc chore!: route connection logs to new table (#18340)
### Breaking Change (changelog note):
> User connections to workspaces, and the opening of workspace apps or ports will no longer create entries in the audit log. Those events will now be included in the 'Connection Log'.
Please see the 'Connection Log' page in the dashboard, and the Connection Log [documentation](https://coder.com/docs/admin/monitoring/connection-logs) for details. Those with permission to view the Audit Log will also be able to view the Connection Log. The new Connection Log has the same licensing restrictions as the Audit Log, and requires a Premium Coder deployment.

### Context

This is the first PR of a few for moving connection events out of the audit log, and into a new database table and web UI page called the 'Connection Log'.

This PR:
- Creates the new table
- Adds and tests queries for inserting and reading, including reading with an RBAC filter.
- Implements the corresponding RBAC changes, such that anyone who can view the audit log can read from the table
- Implements, under the enterprise package, a `ConnectionLogger` abstraction to replace the `Auditor` abstraction for these logs. (No-op'd in AGPL, like the `Auditor`)
- Routes SSH connection and Workspace App events into the new `ConnectionLogger`
- Updates all existing tests to check the values of the `ConnectionLogger` instead of the `Auditor`.

Future PRs:
- Add filtering to the query
- Add an enterprise endpoint to query the new table
- Write a query to delete old events from the audit log, call it from dbpurge.
- Implement a table in the Web UI for viewing connection logs.


> [!NOTE]
> The PRs in this stack obviously won't be (completely) atomic. Whilst they'll each pass CI, the stack is designed to be merged all at once. I'm splitting them up for the sake of those reviewing, and so changes can be reviewed as early as possible.  Despite this, it's really hard to make this PR any smaller than it already is. I'll be keeping it in draft until it's actually ready to merge.
2025-07-15 14:36:06 +10:00

899 lines
33 KiB
Go

package license_test
import (
"context"
"fmt"
"slices"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
)
func TestEntitlements(t *testing.T) {
t.Parallel()
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, _ := dbtestutil.NewDB(t)
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
for _, featureName := range codersdk.FeatureNames {
require.False(t, entitlements.Features[featureName].Enabled)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
})
t.Run("Always return the current user count", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
require.Equal(t, *entitlements.Features[codersdk.FeatureUserLimit].Actual, int64(0))
})
t.Run("SingleLicenseNothing", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{}),
Exp: dbtime.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, empty)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
for _, featureName := range codersdk.FeatureNames {
require.False(t, entitlements.Features[featureName].Enabled)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
})
t.Run("SingleLicenseAll", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: func() license.Features {
f := make(license.Features)
for _, name := range codersdk.FeatureNames {
f[name] = 1
}
return f
}(),
}),
Exp: dbtime.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, empty)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
for _, featureName := range codersdk.FeatureNames {
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[featureName].Entitlement, featureName)
}
})
t.Run("SingleLicenseGrace", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
GraceAt: dbtime.Now().Add(-time.Hour),
ExpiresAt: dbtime.Now().Add(time.Hour),
}),
Exp: dbtime.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
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("Expiration warning", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
GraceAt: dbtime.Now().AddDate(0, 0, 2),
ExpiresAt: dbtime.Now().AddDate(0, 0, 5),
}),
Exp: dbtime.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
require.Contains(
t, entitlements.Warnings,
"Your license expires in 2 days.",
)
})
t.Run("Expiration warning for license expiring in 1 day", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
GraceAt: dbtime.Now().AddDate(0, 0, 1),
ExpiresAt: dbtime.Now().AddDate(0, 0, 5),
}),
Exp: time.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
require.Contains(
t, entitlements.Warnings,
"Your license expires in 1 day.",
)
})
t.Run("Expiration warning for trials", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
Trial: true,
GraceAt: dbtime.Now().AddDate(0, 0, 8),
ExpiresAt: dbtime.Now().AddDate(0, 0, 5),
}),
Exp: dbtime.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.True(t, entitlements.Trial)
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
require.NotContains( // it should not contain a warning since it is a trial license
t, entitlements.Warnings,
"Your license expires in 8 days.",
)
})
t.Run("Expiration warning for non trials", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
GraceAt: dbtime.Now().AddDate(0, 0, 30),
ExpiresAt: dbtime.Now().AddDate(0, 0, 5),
}),
Exp: dbtime.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
require.NotContains( // it should not contain a warning since it is a trial license
t, entitlements.Warnings,
"Your license expires in 30 days.",
)
})
t.Run("SingleLicenseNotEntitled", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
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.FeatureMultipleExternalAuth {
continue
}
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)
require.Contains(t, entitlements.Warnings, fmt.Sprintf("%s is enabled but your license is not entitled to this feature.", niceName))
}
})
t.Run("TooManyUsers", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
activeUser1, err := db.InsertUser(context.Background(), database.InsertUserParams{
ID: uuid.New(),
Username: "test1",
Email: "test1@coder.com",
LoginType: database.LoginTypePassword,
RBACRoles: []string{},
})
require.NoError(t, err)
_, err = db.UpdateUserStatus(context.Background(), database.UpdateUserStatusParams{
ID: activeUser1.ID,
Status: database.UserStatusActive,
UpdatedAt: dbtime.Now(),
})
require.NoError(t, err)
activeUser2, err := db.InsertUser(context.Background(), database.InsertUserParams{
ID: uuid.New(),
Username: "test2",
Email: "test2@coder.com",
LoginType: database.LoginTypePassword,
RBACRoles: []string{},
})
require.NoError(t, err)
_, err = db.UpdateUserStatus(context.Background(), database.UpdateUserStatusParams{
ID: activeUser2.ID,
Status: database.UserStatusActive,
UpdatedAt: dbtime.Now(),
})
require.NoError(t, err)
_, err = db.InsertUser(context.Background(), database.InsertUserParams{
ID: uuid.New(),
Username: "dormant-user",
Email: "dormant-user@coder.com",
LoginType: database.LoginTypePassword,
RBACRoles: []string{},
})
require.NoError(t, err)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 1,
},
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, 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.")
})
t.Run("MaximizeUserLimit", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertUser(context.Background(), database.InsertUserParams{})
db.InsertUser(context.Background(), database.InsertUserParams{})
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 10,
},
GraceAt: time.Now().Add(59 * 24 * time.Hour),
}),
Exp: time.Now().Add(60 * 24 * time.Hour),
})
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 1,
},
GraceAt: time.Now().Add(59 * 24 * time.Hour),
}),
Exp: time.Now().Add(60 * 24 * time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, empty)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Empty(t, entitlements.Warnings)
})
t.Run("MultipleLicenseEnabled", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
// One trial
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Trial: true,
}),
})
// One not
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Trial: false,
}),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, empty)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
})
t.Run("Enterprise", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
FeatureSet: codersdk.FeatureSetEnterprise,
}),
})
require.NoError(t, err)
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
// All enterprise features should be entitled
enterpriseFeatures := codersdk.FeatureSetEnterprise.Features()
for _, featureName := range codersdk.FeatureNames {
if featureName == codersdk.FeatureUserLimit {
continue
}
if slices.Contains(enterpriseFeatures, featureName) {
require.True(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[featureName].Entitlement)
} else {
require.False(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
}
})
t.Run("Premium", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
FeatureSet: codersdk.FeatureSetPremium,
}),
})
require.NoError(t, err)
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
// All premium features should be entitled
enterpriseFeatures := codersdk.FeatureSetPremium.Features()
for _, featureName := range codersdk.FeatureNames {
if featureName == codersdk.FeatureUserLimit {
continue
}
if slices.Contains(enterpriseFeatures, featureName) {
require.True(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[featureName].Entitlement)
} else {
require.False(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
}
})
t.Run("SetNone", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
_, err := db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
FeatureSet: "",
}),
})
require.NoError(t, err)
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
for _, featureName := range codersdk.FeatureNames {
require.False(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
})
// AllFeatures uses the deprecated 'AllFeatures' boolean.
t.Run("AllFeatures", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
AllFeatures: true,
}),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
// All enterprise features should be entitled
enterpriseFeatures := codersdk.FeatureSetEnterprise.Features()
for _, featureName := range codersdk.FeatureNames {
if featureName == codersdk.FeatureUserLimit {
continue
}
if slices.Contains(enterpriseFeatures, featureName) {
require.True(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementEntitled, entitlements.Features[featureName].Entitlement)
} else {
require.False(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
}
})
t.Run("AllFeaturesAlwaysEnable", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: dbtime.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
AllFeatures: true,
}),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, empty)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
// All enterprise features should be entitled
enterpriseFeatures := codersdk.FeatureSetEnterprise.Features()
for _, featureName := range codersdk.FeatureNames {
if featureName == codersdk.FeatureUserLimit {
continue
}
feature := entitlements.Features[featureName]
if slices.Contains(enterpriseFeatures, featureName) {
require.Equal(t, featureName.AlwaysEnable(), feature.Enabled)
require.Equal(t, codersdk.EntitlementEntitled, feature.Entitlement)
} else {
require.False(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
}
})
t.Run("AllFeaturesGrace", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: dbtime.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
AllFeatures: true,
GraceAt: dbtime.Now().Add(-time.Hour),
ExpiresAt: dbtime.Now().Add(time.Hour),
}),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
// All enterprise features should be entitled
enterpriseFeatures := codersdk.FeatureSetEnterprise.Features()
for _, featureName := range codersdk.FeatureNames {
if featureName == codersdk.FeatureUserLimit {
continue
}
if slices.Contains(enterpriseFeatures, featureName) {
require.True(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementGracePeriod, entitlements.Features[featureName].Entitlement)
} else {
require.False(t, entitlements.Features[featureName].Enabled, featureName)
require.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
}
})
t.Run("MultipleReplicasNoLicense", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
entitlements, err := license.Entitlements(context.Background(), db, 2, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.Len(t, entitlements.Errors, 1)
require.Equal(t, "You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.", entitlements.Errors[0])
})
t.Run("MultipleReplicasNotEntitled", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAuditLog: 1,
},
}),
})
entitlements, err := license.Entitlements(context.Background(), db, 2, 1, coderdenttest.Keys, map[codersdk.FeatureName]bool{
codersdk.FeatureHighAvailability: true,
})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Len(t, entitlements.Errors, 1)
require.Equal(t, "You have multiple replicas but your license is not entitled to high availability. You will be unable to connect to workspaces.", entitlements.Errors[0])
})
t.Run("MultipleReplicasGrace", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
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, 2, 1, coderdenttest.Keys, map[codersdk.FeatureName]bool{
codersdk.FeatureHighAvailability: true,
})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Len(t, entitlements.Warnings, 1)
require.Equal(t, "You have multiple replicas but your license for high availability is expired. Reduce to one replica or workspace connections will stop working.", entitlements.Warnings[0])
})
t.Run("MultipleGitAuthNoLicense", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
entitlements, err := license.Entitlements(context.Background(), db, 1, 2, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.Len(t, entitlements.Errors, 1)
require.Equal(t, "You have multiple External Auth Providers configured but this is an Enterprise feature. Reduce to one.", entitlements.Errors[0])
})
t.Run("MultipleGitAuthNotEntitled", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAuditLog: 1,
},
}),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 2, coderdenttest.Keys, map[codersdk.FeatureName]bool{
codersdk.FeatureMultipleExternalAuth: true,
})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Len(t, entitlements.Errors, 1)
require.Equal(t, "You have multiple External Auth Providers configured but your license is limited at one.", entitlements.Errors[0])
})
t.Run("MultipleGitAuthGrace", func(t *testing.T) {
t.Parallel()
db, _ := dbtestutil.NewDB(t)
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
GraceAt: time.Now().Add(-time.Hour),
ExpiresAt: time.Now().Add(time.Hour),
Features: license.Features{
codersdk.FeatureMultipleExternalAuth: 1,
},
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, 1, 2, coderdenttest.Keys, map[codersdk.FeatureName]bool{
codersdk.FeatureMultipleExternalAuth: true,
})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Len(t, entitlements.Warnings, 1)
require.Equal(t, "You have multiple External Auth Providers configured but your license is expired. Reduce to one.", entitlements.Warnings[0])
})
}
func TestLicenseEntitlements(t *testing.T) {
t.Parallel()
// We must use actual 'time.Now()' in tests because the jwt library does
// not accept a custom time function. The only way to change it is as a
// package global, which does not work in t.Parallel().
// This list comes from coderd.go on launch. This list is a bit arbitrary,
// maybe some should be moved to "AlwaysEnabled" instead.
defaultEnablements := map[codersdk.FeatureName]bool{
codersdk.FeatureAuditLog: true,
codersdk.FeatureConnectionLog: true,
codersdk.FeatureBrowserOnly: true,
codersdk.FeatureSCIM: true,
codersdk.FeatureMultipleExternalAuth: true,
codersdk.FeatureTemplateRBAC: true,
codersdk.FeatureExternalTokenEncryption: true,
codersdk.FeatureExternalProvisionerDaemons: true,
codersdk.FeatureAdvancedTemplateScheduling: true,
codersdk.FeatureWorkspaceProxy: true,
codersdk.FeatureUserRoleManagement: true,
codersdk.FeatureAccessControl: true,
codersdk.FeatureControlSharedPorts: true,
}
legacyLicense := func() *coderdenttest.LicenseOptions {
return (&coderdenttest.LicenseOptions{
AccountType: "salesforce",
AccountID: "Alice",
Trial: false,
// Use the legacy boolean
AllFeatures: true,
}).Valid(time.Now())
}
enterpriseLicense := func() *coderdenttest.LicenseOptions {
return (&coderdenttest.LicenseOptions{
AccountType: "salesforce",
AccountID: "Bob",
DeploymentIDs: nil,
Trial: false,
FeatureSet: codersdk.FeatureSetEnterprise,
AllFeatures: true,
}).Valid(time.Now())
}
premiumLicense := func() *coderdenttest.LicenseOptions {
return (&coderdenttest.LicenseOptions{
AccountType: "salesforce",
AccountID: "Charlie",
DeploymentIDs: nil,
Trial: false,
FeatureSet: codersdk.FeatureSetPremium,
AllFeatures: true,
}).Valid(time.Now())
}
testCases := []struct {
Name string
Licenses []*coderdenttest.LicenseOptions
Enablements map[codersdk.FeatureName]bool
Arguments license.FeatureArguments
ExpectedErrorContains string
AssertEntitlements func(t *testing.T, entitlements codersdk.Entitlements)
}{
{
Name: "NoLicenses",
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assertNoErrors(t, entitlements)
assertNoWarnings(t, entitlements)
assert.False(t, entitlements.HasLicense)
assert.False(t, entitlements.Trial)
},
},
{
Name: "MixedUsedCounts",
Licenses: []*coderdenttest.LicenseOptions{
legacyLicense().UserLimit(100),
enterpriseLicense().UserLimit(500),
},
Enablements: defaultEnablements,
Arguments: license.FeatureArguments{
ActiveUserCount: 50,
ReplicaCount: 0,
ExternalAuthCount: 0,
},
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assertEnterpriseFeatures(t, entitlements)
assertNoErrors(t, entitlements)
assertNoWarnings(t, entitlements)
userFeature := entitlements.Features[codersdk.FeatureUserLimit]
assert.Equalf(t, int64(500), *userFeature.Limit, "user limit")
assert.Equalf(t, int64(50), *userFeature.Actual, "user count")
},
},
{
Name: "MixedUsedCountsWithExpired",
Licenses: []*coderdenttest.LicenseOptions{
// This license is ignored
enterpriseLicense().UserLimit(500).Expired(time.Now()),
enterpriseLicense().UserLimit(100),
},
Enablements: defaultEnablements,
Arguments: license.FeatureArguments{
ActiveUserCount: 200,
ReplicaCount: 0,
ExternalAuthCount: 0,
},
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assertEnterpriseFeatures(t, entitlements)
userFeature := entitlements.Features[codersdk.FeatureUserLimit]
assert.Equalf(t, int64(100), *userFeature.Limit, "user limit")
assert.Equalf(t, int64(200), *userFeature.Actual, "user count")
require.Len(t, entitlements.Errors, 1, "invalid license error")
require.Len(t, entitlements.Warnings, 1, "user count exceeds warning")
require.Contains(t, entitlements.Errors[0], "Invalid license")
require.Contains(t, entitlements.Warnings[0], "active users but is only licensed for")
},
},
{
// The new license does not have enough seats to cover the active user count.
// The old license is in it's grace period.
Name: "MixedUsedCountsWithGrace",
Licenses: []*coderdenttest.LicenseOptions{
enterpriseLicense().UserLimit(500).GracePeriod(time.Now()),
enterpriseLicense().UserLimit(100),
},
Enablements: defaultEnablements,
Arguments: license.FeatureArguments{
ActiveUserCount: 200,
ReplicaCount: 0,
ExternalAuthCount: 0,
},
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
userFeature := entitlements.Features[codersdk.FeatureUserLimit]
assert.Equalf(t, int64(500), *userFeature.Limit, "user limit")
assert.Equalf(t, int64(200), *userFeature.Actual, "user count")
assert.Equal(t, userFeature.Entitlement, codersdk.EntitlementGracePeriod)
},
},
{
// Legacy license uses the "AllFeatures" boolean
Name: "LegacyLicense",
Licenses: []*coderdenttest.LicenseOptions{
legacyLicense().UserLimit(100),
},
Enablements: defaultEnablements,
Arguments: license.FeatureArguments{
ActiveUserCount: 50,
ReplicaCount: 0,
ExternalAuthCount: 0,
},
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assertEnterpriseFeatures(t, entitlements)
assertNoErrors(t, entitlements)
assertNoWarnings(t, entitlements)
userFeature := entitlements.Features[codersdk.FeatureUserLimit]
assert.Equalf(t, int64(100), *userFeature.Limit, "user limit")
assert.Equalf(t, int64(50), *userFeature.Actual, "user count")
},
},
{
Name: "EnterpriseDisabledMultiOrg",
Licenses: []*coderdenttest.LicenseOptions{
enterpriseLicense().UserLimit(100),
},
Enablements: defaultEnablements,
Arguments: license.FeatureArguments{},
ExpectedErrorContains: "",
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assert.False(t, entitlements.Features[codersdk.FeatureMultipleOrganizations].Enabled, "multi-org only enabled for premium")
assert.False(t, entitlements.Features[codersdk.FeatureCustomRoles].Enabled, "custom-roles only enabled for premium")
},
},
{
Name: "PremiumEnabledMultiOrg",
Licenses: []*coderdenttest.LicenseOptions{
premiumLicense().UserLimit(100),
},
Enablements: defaultEnablements,
Arguments: license.FeatureArguments{},
ExpectedErrorContains: "",
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assert.True(t, entitlements.Features[codersdk.FeatureMultipleOrganizations].Enabled, "multi-org enabled for premium")
assert.True(t, entitlements.Features[codersdk.FeatureCustomRoles].Enabled, "custom-roles enabled for premium")
},
},
{
Name: "CurrentAndFuture",
Licenses: []*coderdenttest.LicenseOptions{
enterpriseLicense().UserLimit(100),
premiumLicense().UserLimit(200).FutureTerm(time.Now()),
},
Enablements: defaultEnablements,
AssertEntitlements: func(t *testing.T, entitlements codersdk.Entitlements) {
assertEnterpriseFeatures(t, entitlements)
assertNoErrors(t, entitlements)
assertNoWarnings(t, entitlements)
userFeature := entitlements.Features[codersdk.FeatureUserLimit]
assert.Equalf(t, int64(100), *userFeature.Limit, "user limit")
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureMultipleOrganizations].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureCustomRoles].Entitlement)
},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
generatedLicenses := make([]database.License, 0, len(tc.Licenses))
for i, lo := range tc.Licenses {
generatedLicenses = append(generatedLicenses, database.License{
ID: int32(i), // nolint:gosec
UploadedAt: time.Now().Add(time.Hour * -1),
JWT: lo.Generate(t),
Exp: lo.GraceAt,
UUID: uuid.New(),
})
}
entitlements, err := license.LicensesEntitlements(time.Now(), generatedLicenses, tc.Enablements, coderdenttest.Keys, tc.Arguments)
if tc.ExpectedErrorContains != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.ExpectedErrorContains)
} else {
require.NoError(t, err)
tc.AssertEntitlements(t, entitlements)
}
})
}
}
func assertNoErrors(t *testing.T, entitlements codersdk.Entitlements) {
assert.Empty(t, entitlements.Errors, "no errors")
}
func assertNoWarnings(t *testing.T, entitlements codersdk.Entitlements) {
assert.Empty(t, entitlements.Warnings, "no warnings")
}
func assertEnterpriseFeatures(t *testing.T, entitlements codersdk.Entitlements) {
for _, expected := range codersdk.FeatureSetEnterprise.Features() {
f := entitlements.Features[expected]
assert.Equalf(t, codersdk.EntitlementEntitled, f.Entitlement, "%s entitled", expected)
assert.Equalf(t, true, f.Enabled, "%s enabled", expected)
}
}