feat: add license expiration warning (#7264)

* wip: add expiration warning

* Use GraceAt

* show expiration warning for trial accounts

* fix test

* only show license banner for users with deployment permission

---------

Co-authored-by: Marcin Tojek <marcin@coder.com>
This commit is contained in:
Rodrigo Maia
2023-04-26 16:39:39 -03:00
committed by GitHub
parent 3eb7f06bf1
commit c3fe2515a7
4 changed files with 143 additions and 3 deletions

View File

@ -55,6 +55,7 @@ func TestEntitlements(t *testing.T) {
codersdk.FeatureAdvancedTemplateScheduling: 1, codersdk.FeatureAdvancedTemplateScheduling: 1,
codersdk.FeatureWorkspaceProxy: 1, codersdk.FeatureWorkspaceProxy: 1,
}, },
GraceAt: time.Now().Add(59 * 24 * time.Hour),
}) })
res, err := client.Entitlements(context.Background()) res, err := client.Entitlements(context.Background())
require.NoError(t, err) require.NoError(t, err)

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"fmt" "fmt"
"math"
"time" "time"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
@ -70,6 +71,23 @@ func Entitlements(
// LicenseExpires we must be in grace period. // LicenseExpires we must be in grace period.
entitlement = codersdk.EntitlementGracePeriod entitlement = codersdk.EntitlementGracePeriod
} }
// Add warning if license is expiring soon
daysToExpire := int(math.Ceil(claims.LicenseExpires.Sub(now).Hours() / 24))
isTrial := entitlements.Trial
showWarningDays := 30
if isTrial {
showWarningDays = 7
}
isExpiringSoon := daysToExpire > 0 && daysToExpire < showWarningDays
if isExpiringSoon {
day := "day"
if daysToExpire > 1 {
day = "days"
}
entitlements.Warnings = append(entitlements.Warnings, fmt.Sprintf("Your license expires in %d %s.", daysToExpire, day))
}
for featureName, featureValue := range claims.Features { for featureName, featureValue := range claims.Features {
// Can this be negative? // Can this be negative?
if featureValue <= 0 { if featureValue <= 0 {

View File

@ -102,6 +102,123 @@ func TestEntitlements(t *testing.T) {
fmt.Sprintf("%s is enabled but your license for this feature is expired.", codersdk.FeatureAuditLog.Humanize()), 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 := dbfake.New()
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
GraceAt: time.Now().AddDate(0, 0, 2),
ExpiresAt: time.Now().AddDate(0, 0, 5),
}),
Exp: time.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 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 := dbfake.New()
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
GraceAt: time.Now().AddDate(0, 0, 1),
ExpiresAt: time.Now().AddDate(0, 0, 5),
}),
Exp: time.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 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 := dbfake.New()
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
Trial: true,
GraceAt: time.Now().AddDate(0, 0, 8),
ExpiresAt: time.Now().AddDate(0, 0, 5),
}),
Exp: time.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 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 := dbfake.New()
db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureUserLimit: 100,
codersdk.FeatureAuditLog: 1,
},
GraceAt: time.Now().AddDate(0, 0, 30),
ExpiresAt: time.Now().AddDate(0, 0, 5),
}),
Exp: time.Now().AddDate(0, 0, 5),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 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.Run("SingleLicenseNotEntitled", func(t *testing.T) {
t.Parallel() t.Parallel()
db := dbfake.New() db := dbfake.New()
@ -164,16 +281,18 @@ func TestEntitlements(t *testing.T) {
Features: license.Features{ Features: license.Features{
codersdk.FeatureUserLimit: 10, codersdk.FeatureUserLimit: 10,
}, },
GraceAt: time.Now().Add(59 * 24 * time.Hour),
}), }),
Exp: time.Now().Add(time.Hour), Exp: time.Now().Add(60 * 24 * time.Hour),
}) })
db.InsertLicense(context.Background(), database.InsertLicenseParams{ db.InsertLicense(context.Background(), database.InsertLicenseParams{
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{ JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
Features: license.Features{ Features: license.Features{
codersdk.FeatureUserLimit: 1, codersdk.FeatureUserLimit: 1,
}, },
GraceAt: time.Now().Add(59 * 24 * time.Hour),
}), }),
Exp: time.Now().Add(time.Hour), Exp: time.Now().Add(60 * 24 * time.Hour),
}) })
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty) entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, empty)
require.NoError(t, err) require.NoError(t, err)

View File

@ -25,10 +25,12 @@ export const DashboardLayout: FC = () => {
}) })
const { error: updateCheckError, updateCheck } = updateCheckState.context const { error: updateCheckError, updateCheck } = updateCheckState.context
const canViewDeployment = Boolean(permissions.viewDeploymentValues)
return ( return (
<DashboardProvider> <DashboardProvider>
<ServiceBanner /> <ServiceBanner />
<LicenseBanner /> {canViewDeployment && <LicenseBanner />}
<div className={styles.site}> <div className={styles.site}>
<Navbar /> <Navbar />