mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: enable key rotation (#15066)
This PR contains the remaining logic necessary to hook up key rotation to the product.
This commit is contained in:
@@ -228,6 +228,42 @@ var (
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
|
||||
// See cryptokeys package.
|
||||
subjectCryptoKeyRotator = rbac.Subject{
|
||||
FriendlyName: "Crypto Key Rotator",
|
||||
ID: uuid.Nil.String(),
|
||||
Roles: rbac.Roles([]rbac.Role{
|
||||
{
|
||||
Identifier: rbac.RoleIdentifier{Name: "keyrotator"},
|
||||
DisplayName: "Key Rotator",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceCryptoKey.Type: {policy.WildcardSymbol},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
},
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
|
||||
// See cryptokeys package.
|
||||
subjectCryptoKeyReader = rbac.Subject{
|
||||
FriendlyName: "Crypto Key Reader",
|
||||
ID: uuid.Nil.String(),
|
||||
Roles: rbac.Roles([]rbac.Role{
|
||||
{
|
||||
Identifier: rbac.RoleIdentifier{Name: "keyrotator"},
|
||||
DisplayName: "Key Rotator",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceCryptoKey.Type: {policy.WildcardSymbol},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
},
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
|
||||
subjectSystemRestricted = rbac.Subject{
|
||||
FriendlyName: "System",
|
||||
ID: uuid.Nil.String(),
|
||||
@@ -281,6 +317,16 @@ func AsHangDetector(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authContextKey{}, subjectHangDetector)
|
||||
}
|
||||
|
||||
// AsKeyRotator returns a context with an actor that has permissions required for rotating crypto keys.
|
||||
func AsKeyRotator(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyRotator)
|
||||
}
|
||||
|
||||
// AsKeyReader returns a context with an actor that has permissions required for reading crypto keys.
|
||||
func AsKeyReader(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyReader)
|
||||
}
|
||||
|
||||
// AsSystemRestricted returns a context with an actor that has permissions
|
||||
// required for various system operations (login, logout, metrics cache).
|
||||
func AsSystemRestricted(ctx context.Context) context.Context {
|
||||
|
||||
@@ -2243,13 +2243,13 @@ func (s *MethodTestSuite) TestCryptoKeys() {
|
||||
}))
|
||||
s.Run("InsertCryptoKey", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.InsertCryptoKeyParams{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
}).
|
||||
Asserts(rbac.ResourceCryptoKey, policy.ActionCreate)
|
||||
}))
|
||||
s.Run("DeleteCryptoKey", s.Subtest(func(db database.Store, check *expects) {
|
||||
key := dbgen.CryptoKey(s.T(), db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: 4,
|
||||
})
|
||||
check.Args(database.DeleteCryptoKeyParams{
|
||||
@@ -2259,7 +2259,7 @@ func (s *MethodTestSuite) TestCryptoKeys() {
|
||||
}))
|
||||
s.Run("GetCryptoKeyByFeatureAndSequence", s.Subtest(func(db database.Store, check *expects) {
|
||||
key := dbgen.CryptoKey(s.T(), db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: 4,
|
||||
})
|
||||
check.Args(database.GetCryptoKeyByFeatureAndSequenceParams{
|
||||
@@ -2269,14 +2269,14 @@ func (s *MethodTestSuite) TestCryptoKeys() {
|
||||
}))
|
||||
s.Run("GetLatestCryptoKeyByFeature", s.Subtest(func(db database.Store, check *expects) {
|
||||
dbgen.CryptoKey(s.T(), db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: 4,
|
||||
})
|
||||
check.Args(database.CryptoKeyFeatureWorkspaceApps).Asserts(rbac.ResourceCryptoKey, policy.ActionRead)
|
||||
check.Args(database.CryptoKeyFeatureWorkspaceAppsAPIKey).Asserts(rbac.ResourceCryptoKey, policy.ActionRead)
|
||||
}))
|
||||
s.Run("UpdateCryptoKeyDeletesAt", s.Subtest(func(db database.Store, check *expects) {
|
||||
key := dbgen.CryptoKey(s.T(), db, database.CryptoKey{
|
||||
Feature: database.CryptoKeyFeatureWorkspaceApps,
|
||||
Feature: database.CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
Sequence: 4,
|
||||
})
|
||||
check.Args(database.UpdateCryptoKeyDeletesAtParams{
|
||||
@@ -2286,7 +2286,7 @@ func (s *MethodTestSuite) TestCryptoKeys() {
|
||||
}).Asserts(rbac.ResourceCryptoKey, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("GetCryptoKeysByFeature", s.Subtest(func(db database.Store, check *expects) {
|
||||
check.Args(database.CryptoKeyFeatureWorkspaceApps).
|
||||
check.Args(database.CryptoKeyFeatureWorkspaceAppsAPIKey).
|
||||
Asserts(rbac.ResourceCryptoKey, policy.ActionRead)
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -943,7 +943,7 @@ func CustomRole(t testing.TB, db database.Store, seed database.CustomRole) datab
|
||||
func CryptoKey(t testing.TB, db database.Store, seed database.CryptoKey) database.CryptoKey {
|
||||
t.Helper()
|
||||
|
||||
seed.Feature = takeFirst(seed.Feature, database.CryptoKeyFeatureWorkspaceApps)
|
||||
seed.Feature = takeFirst(seed.Feature, database.CryptoKeyFeatureWorkspaceAppsAPIKey)
|
||||
|
||||
// An empty string for the secret is interpreted as
|
||||
// a caller wanting a new secret to be generated.
|
||||
@@ -1048,9 +1048,11 @@ func takeFirst[Value comparable](values ...Value) Value {
|
||||
|
||||
func newCryptoKeySecret(feature database.CryptoKeyFeature) (string, error) {
|
||||
switch feature {
|
||||
case database.CryptoKeyFeatureWorkspaceApps:
|
||||
case database.CryptoKeyFeatureWorkspaceAppsAPIKey:
|
||||
return generateCryptoKey(32)
|
||||
case database.CryptoKeyFeatureOidcConvert:
|
||||
case database.CryptoKeyFeatureWorkspaceAppsToken:
|
||||
return generateCryptoKey(64)
|
||||
case database.CryptoKeyFeatureOIDCConvert:
|
||||
return generateCryptoKey(64)
|
||||
case database.CryptoKeyFeatureTailnetResume:
|
||||
return generateCryptoKey(64)
|
||||
|
||||
Generated
+2
-1
@@ -38,7 +38,8 @@ CREATE TYPE build_reason AS ENUM (
|
||||
);
|
||||
|
||||
CREATE TYPE crypto_key_feature AS ENUM (
|
||||
'workspace_apps',
|
||||
'workspace_apps_token',
|
||||
'workspace_apps_api_key',
|
||||
'oidc_convert',
|
||||
'tailnet_resume'
|
||||
);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Step 1: Remove the new entries from crypto_keys table
|
||||
DELETE FROM crypto_keys
|
||||
WHERE feature IN ('workspace_apps_token', 'workspace_apps_api_key');
|
||||
|
||||
CREATE TYPE old_crypto_key_feature AS ENUM (
|
||||
'workspace_apps',
|
||||
'oidc_convert',
|
||||
'tailnet_resume'
|
||||
);
|
||||
|
||||
ALTER TABLE crypto_keys
|
||||
ALTER COLUMN feature TYPE old_crypto_key_feature
|
||||
USING (feature::text::old_crypto_key_feature);
|
||||
|
||||
DROP TYPE crypto_key_feature;
|
||||
|
||||
ALTER TYPE old_crypto_key_feature RENAME TO crypto_key_feature;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Create a new enum type with the desired values
|
||||
CREATE TYPE new_crypto_key_feature AS ENUM (
|
||||
'workspace_apps_token',
|
||||
'workspace_apps_api_key',
|
||||
'oidc_convert',
|
||||
'tailnet_resume'
|
||||
);
|
||||
|
||||
DELETE FROM crypto_keys WHERE feature = 'workspace_apps';
|
||||
|
||||
-- Drop the old type and rename the new one
|
||||
ALTER TABLE crypto_keys
|
||||
ALTER COLUMN feature TYPE new_crypto_key_feature
|
||||
USING (feature::text::new_crypto_key_feature);
|
||||
|
||||
DROP TYPE crypto_key_feature;
|
||||
|
||||
ALTER TYPE new_crypto_key_feature RENAME TO crypto_key_feature;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
INSERT INTO crypto_keys (feature, sequence, secret, secret_key_id, starts_at, deletes_at)
|
||||
VALUES (
|
||||
'workspace_apps_token',
|
||||
1,
|
||||
'abc',
|
||||
NULL,
|
||||
'1970-01-01 00:00:00 UTC'::timestamptz,
|
||||
'2100-01-01 00:00:00 UTC'::timestamptz
|
||||
);
|
||||
|
||||
INSERT INTO crypto_keys (feature, sequence, secret, secret_key_id, starts_at, deletes_at)
|
||||
VALUES (
|
||||
'workspace_apps_api_key',
|
||||
1,
|
||||
'def',
|
||||
NULL,
|
||||
'1970-01-01 00:00:00 UTC'::timestamptz,
|
||||
'2100-01-01 00:00:00 UTC'::timestamptz
|
||||
);
|
||||
|
||||
INSERT INTO crypto_keys (feature, sequence, secret, secret_key_id, starts_at, deletes_at)
|
||||
VALUES (
|
||||
'oidc_convert',
|
||||
2,
|
||||
'ghi',
|
||||
NULL,
|
||||
'1970-01-01 00:00:00 UTC'::timestamptz,
|
||||
'2100-01-01 00:00:00 UTC'::timestamptz
|
||||
);
|
||||
|
||||
INSERT INTO crypto_keys (feature, sequence, secret, secret_key_id, starts_at, deletes_at)
|
||||
VALUES (
|
||||
'tailnet_resume',
|
||||
2,
|
||||
'jkl',
|
||||
NULL,
|
||||
'1970-01-01 00:00:00 UTC'::timestamptz,
|
||||
'2100-01-01 00:00:00 UTC'::timestamptz
|
||||
);
|
||||
|
||||
@@ -345,9 +345,10 @@ func AllBuildReasonValues() []BuildReason {
|
||||
type CryptoKeyFeature string
|
||||
|
||||
const (
|
||||
CryptoKeyFeatureWorkspaceApps CryptoKeyFeature = "workspace_apps"
|
||||
CryptoKeyFeatureOidcConvert CryptoKeyFeature = "oidc_convert"
|
||||
CryptoKeyFeatureTailnetResume CryptoKeyFeature = "tailnet_resume"
|
||||
CryptoKeyFeatureWorkspaceAppsToken CryptoKeyFeature = "workspace_apps_token"
|
||||
CryptoKeyFeatureWorkspaceAppsAPIKey CryptoKeyFeature = "workspace_apps_api_key"
|
||||
CryptoKeyFeatureOIDCConvert CryptoKeyFeature = "oidc_convert"
|
||||
CryptoKeyFeatureTailnetResume CryptoKeyFeature = "tailnet_resume"
|
||||
)
|
||||
|
||||
func (e *CryptoKeyFeature) Scan(src interface{}) error {
|
||||
@@ -387,8 +388,9 @@ func (ns NullCryptoKeyFeature) Value() (driver.Value, error) {
|
||||
|
||||
func (e CryptoKeyFeature) Valid() bool {
|
||||
switch e {
|
||||
case CryptoKeyFeatureWorkspaceApps,
|
||||
CryptoKeyFeatureOidcConvert,
|
||||
case CryptoKeyFeatureWorkspaceAppsToken,
|
||||
CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
CryptoKeyFeatureOIDCConvert,
|
||||
CryptoKeyFeatureTailnetResume:
|
||||
return true
|
||||
}
|
||||
@@ -397,8 +399,9 @@ func (e CryptoKeyFeature) Valid() bool {
|
||||
|
||||
func AllCryptoKeyFeatureValues() []CryptoKeyFeature {
|
||||
return []CryptoKeyFeature{
|
||||
CryptoKeyFeatureWorkspaceApps,
|
||||
CryptoKeyFeatureOidcConvert,
|
||||
CryptoKeyFeatureWorkspaceAppsToken,
|
||||
CryptoKeyFeatureWorkspaceAppsAPIKey,
|
||||
CryptoKeyFeatureOIDCConvert,
|
||||
CryptoKeyFeatureTailnetResume,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ sql:
|
||||
api_key_id: APIKeyID
|
||||
callback_url: CallbackURL
|
||||
login_type_oauth2_provider_app: LoginTypeOAuth2ProviderApp
|
||||
crypto_key_feature_workspace_apps_api_key: CryptoKeyFeatureWorkspaceAppsAPIKey
|
||||
crypto_key_feature_oidc_convert: CryptoKeyFeatureOIDCConvert
|
||||
rules:
|
||||
- name: do-not-use-public-schema-in-queries
|
||||
message: "do not use public schema in queries"
|
||||
|
||||
Reference in New Issue
Block a user