Skip to content

feat: Add Git auth for GitHub, GitLab, Azure DevOps, and BitBucket #4670

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add banner for too many configurations
  • Loading branch information
kylecarbs committed Oct 24, 2022
commit f2c098367af8fcddba4977a7b5a84789be635989
2 changes: 2 additions & 0 deletions codersdk/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
FeatureWorkspaceQuota = "workspace_quota"
FeatureTemplateRBAC = "template_rbac"
FeatureHighAvailability = "high_availability"
FeatureMultipleGitAuth = "multiple_git_auth"
)

var FeatureNames = []string{
Expand All @@ -32,6 +33,7 @@ var FeatureNames = []string{
FeatureWorkspaceQuota,
FeatureTemplateRBAC,
FeatureHighAvailability,
FeatureMultipleGitAuth,
}

type Feature struct {
Expand Down
18 changes: 3 additions & 15 deletions enterprise/cli/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,10 @@ func TestFeaturesList(t *testing.T) {
var entitlements codersdk.Entitlements
err := json.Unmarshal(buf.Bytes(), &entitlements)
require.NoError(t, err, "unmarshal JSON output")
assert.Len(t, entitlements.Features, 7)
assert.Empty(t, entitlements.Warnings)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureUserLimit].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureAuditLog].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureBrowserOnly].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureWorkspaceQuota].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureTemplateRBAC].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureSCIM].Entitlement)
assert.Equal(t, codersdk.EntitlementNotEntitled,
entitlements.Features[codersdk.FeatureHighAvailability].Entitlement)
for _, featureName := range codersdk.FeatureNames {
assert.Equal(t, codersdk.EntitlementNotEntitled, entitlements.Features[featureName].Entitlement)
}
assert.False(t, entitlements.HasLicense)
assert.False(t, entitlements.Experimental)
})
Expand Down
3 changes: 2 additions & 1 deletion enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,13 @@ 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()), api.Keys, map[string]bool{
entitlements, err := license.Entitlements(ctx, api.Database, api.Logger, len(api.replicaManager.All()), len(api.GitAuthConfigs), api.Keys, map[string]bool{
codersdk.FeatureAuditLog: api.AuditLogging,
codersdk.FeatureBrowserOnly: api.BrowserOnly,
codersdk.FeatureSCIM: len(api.SCIMAPIKey) != 0,
codersdk.FeatureWorkspaceQuota: api.UserWorkspaceQuota != 0,
codersdk.FeatureHighAvailability: api.DERPServerRelayAddress != "",
codersdk.FeatureMultipleGitAuth: len(api.GitAuthConfigs) > 1,
codersdk.FeatureTemplateRBAC: api.RBAC,
})
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions enterprise/coderd/coderdenttest/coderdenttest.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ type LicenseOptions struct {
WorkspaceQuota bool
TemplateRBAC bool
HighAvailability bool
MultipleGitAuth bool
}

// AddLicense generates a new license with the options provided and inserts it.
Expand Down Expand Up @@ -158,6 +159,11 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
rbacEnabled = 1
}

multipleGitAuth := int64(0)
if options.MultipleGitAuth {
multipleGitAuth = 1
}

c := &license.Claims{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "test@testing.test",
Expand All @@ -179,6 +185,7 @@ func GenerateLicense(t *testing.T, options LicenseOptions) string {
WorkspaceQuota: workspaceQuota,
HighAvailability: highAvailability,
TemplateRBAC: rbacEnabled,
MultipleGitAuth: multipleGitAuth,
},
}
tok := jwt.NewWithClaims(jwt.SigningMethodEdDSA, c)
Expand Down
37 changes: 35 additions & 2 deletions enterprise/coderd/license/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func Entitlements(
db database.Store,
logger slog.Logger,
replicaCount int,
gitAuthCount int,
keys map[string]ed25519.PublicKey,
enablements map[string]bool,
) (codersdk.Entitlements, error) {
Expand Down Expand Up @@ -116,6 +117,12 @@ func Entitlements(
Enabled: enablements[codersdk.FeatureTemplateRBAC],
}
}
if claims.Features.MultipleGitAuth > 0 {
entitlements.Features[codersdk.FeatureMultipleGitAuth] = codersdk.Feature{
Entitlement: entitlement,
Enabled: true,
}
}
if claims.AllFeatures {
allFeatures = true
}
Expand Down Expand Up @@ -150,6 +157,10 @@ func Entitlements(
if featureName == codersdk.FeatureHighAvailability {
continue
}
// Multiple Git auth has it's own warnings based on the number configured!
if featureName == codersdk.FeatureMultipleGitAuth {
continue
}
feature := entitlements.Features[featureName]
if !feature.Enabled {
continue
Expand All @@ -173,10 +184,10 @@ func Entitlements(
switch feature.Entitlement {
case codersdk.EntitlementNotEntitled:
if entitlements.HasLicense {
entitlements.Errors = append(entitlements.Warnings,
entitlements.Errors = append(entitlements.Errors,
"You have multiple replicas but your license is not entitled to high availability. You will be unable to connect to workspaces.")
} else {
entitlements.Errors = append(entitlements.Warnings,
entitlements.Errors = append(entitlements.Errors,
"You have multiple replicas but high availability is an Enterprise feature. You will be unable to connect to workspaces.")
}
case codersdk.EntitlementGracePeriod:
Expand All @@ -185,6 +196,27 @@ func Entitlements(
}
}

if gitAuthCount > 1 {
feature := entitlements.Features[codersdk.FeatureMultipleGitAuth]

switch feature.Entitlement {
case codersdk.EntitlementNotEntitled:
if entitlements.HasLicense {
entitlements.Errors = append(entitlements.Errors,
"You have multiple Git authorizations configured but your license is limited at one.",
)
} else {
entitlements.Errors = append(entitlements.Errors,
"You have multiple Git authorizations configured but this is an Enterprise feature. Reduce to one.",
)
}
case codersdk.EntitlementGracePeriod:
entitlements.Warnings = append(entitlements.Warnings,
"You have multiple Git authorizations configured but your license is expired. Reduce to one.",
)
}
}

for _, featureName := range codersdk.FeatureNames {
feature := entitlements.Features[featureName]
if feature.Entitlement == codersdk.EntitlementNotEntitled {
Expand Down Expand Up @@ -219,6 +251,7 @@ type Features struct {
WorkspaceQuota int64 `json:"workspace_quota"`
TemplateRBAC int64 `json:"template_rbac"`
HighAvailability int64 `json:"high_availability"`
MultipleGitAuth int64 `json:"multiple_git_auth"`
}

type Claims struct {
Expand Down
78 changes: 66 additions & 12 deletions enterprise/coderd/license/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestEntitlements(t *testing.T) {
t.Run("Defaults", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, all)
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
Expand All @@ -47,7 +47,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, coderdenttest.Keys, map[string]bool{})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
Expand All @@ -71,7 +71,7 @@ func TestEntitlements(t *testing.T) {
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
Expand All @@ -96,7 +96,7 @@ func TestEntitlements(t *testing.T) {
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, all)
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)
Expand All @@ -107,6 +107,9 @@ func TestEntitlements(t *testing.T) {
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))
Expand All @@ -119,7 +122,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, coderdenttest.Keys, all)
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)
Expand All @@ -130,6 +133,9 @@ func TestEntitlements(t *testing.T) {
if featureName == codersdk.FeatureHighAvailability {
continue
}
if featureName == codersdk.FeatureMultipleGitAuth {
continue
}
niceName := strings.Title(strings.ReplaceAll(featureName, "_", " "))
// Ensures features that are not entitled are properly disabled.
require.False(t, entitlements.Features[featureName].Enabled)
Expand All @@ -152,7 +158,7 @@ func TestEntitlements(t *testing.T) {
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{})
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.")
Expand All @@ -174,7 +180,7 @@ func TestEntitlements(t *testing.T) {
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Empty(t, entitlements.Warnings)
Expand All @@ -197,7 +203,7 @@ func TestEntitlements(t *testing.T) {
}),
})

entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, map[string]bool{})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 1, coderdenttest.Keys, map[string]bool{})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.False(t, entitlements.Trial)
Expand All @@ -212,7 +218,7 @@ func TestEntitlements(t *testing.T) {
AllFeatures: true,
}),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, coderdenttest.Keys, all)
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)
Expand All @@ -228,7 +234,7 @@ func TestEntitlements(t *testing.T) {
t.Run("MultipleReplicasNoLicense", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, coderdenttest.Keys, all)
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, 1, coderdenttest.Keys, all)
require.NoError(t, err)
require.False(t, entitlements.HasLicense)
require.Len(t, entitlements.Errors, 1)
Expand All @@ -244,7 +250,7 @@ func TestEntitlements(t *testing.T) {
AuditLog: true,
}),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, coderdenttest.Keys, map[string]bool{
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, 1, coderdenttest.Keys, map[string]bool{
codersdk.FeatureHighAvailability: true,
})
require.NoError(t, err)
Expand All @@ -264,12 +270,60 @@ func TestEntitlements(t *testing.T) {
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, coderdenttest.Keys, map[string]bool{
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 2, 1, coderdenttest.Keys, map[string]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 := databasefake.New()
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 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 Git authorizations configured but this is an Enterprise feature. Reduce to one.", entitlements.Errors[0])
})

t.Run("MultipleGitAuthNotEntitled", func(t *testing.T) {
t.Parallel()
db := databasefake.New()
db.InsertLicense(context.Background(), database.InsertLicenseParams{
Exp: time.Now().Add(time.Hour),
JWT: coderdenttest.GenerateLicense(t, coderdenttest.LicenseOptions{
AuditLog: true,
}),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 2, coderdenttest.Keys, map[string]bool{
codersdk.FeatureMultipleGitAuth: true,
})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Len(t, entitlements.Errors, 1)
require.Equal(t, "You have multiple Git authorizations configured but your license is limited at one.", entitlements.Errors[0])
})

t.Run("MultipleGitAuthGrace", func(t *testing.T) {
t.Parallel()
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),
}),
Exp: time.Now().Add(time.Hour),
})
entitlements, err := license.Entitlements(context.Background(), db, slog.Logger{}, 1, 2, coderdenttest.Keys, map[string]bool{
codersdk.FeatureMultipleGitAuth: true,
})
require.NoError(t, err)
require.True(t, entitlements.HasLicense)
require.Len(t, entitlements.Warnings, 1)
require.Equal(t, "You have multiple Git authorizations configured but your license is expired. Reduce to one.", entitlements.Warnings[0])
})
}