feat: add schema for key rotation (#14662)

This commit is contained in:
Jon Ayers
2024-09-17 18:08:18 +01:00
committed by GitHub
parent 45420b95f3
commit 45160c7679
29 changed files with 977 additions and 2 deletions
+2
View File
@@ -12027,6 +12027,7 @@ const docTemplate = `{
"assign_org_role",
"assign_role",
"audit_log",
"crypto_key",
"debug_info",
"deployment_config",
"deployment_stats",
@@ -12059,6 +12060,7 @@ const docTemplate = `{
"ResourceAssignOrgRole",
"ResourceAssignRole",
"ResourceAuditLog",
"ResourceCryptoKey",
"ResourceDebugInfo",
"ResourceDeploymentConfig",
"ResourceDeploymentStats",
+2
View File
@@ -10859,6 +10859,7 @@
"assign_org_role",
"assign_role",
"audit_log",
"crypto_key",
"debug_info",
"deployment_config",
"deployment_stats",
@@ -10891,6 +10892,7 @@
"ResourceAssignOrgRole",
"ResourceAssignRole",
"ResourceAuditLog",
"ResourceCryptoKey",
"ResourceDebugInfo",
"ResourceDeploymentConfig",
"ResourceDeploymentStats",
+43
View File
@@ -253,6 +253,7 @@ var (
rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationPreference.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceNotificationTemplate.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceCryptoKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
}),
Org: map[string][]rbac.Permission{},
User: []rbac.Permission{},
@@ -1041,6 +1042,13 @@ func (q *querier) DeleteCoordinator(ctx context.Context, id uuid.UUID) error {
return q.db.DeleteCoordinator(ctx, id)
}
func (q *querier) DeleteCryptoKey(ctx context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceCryptoKey); err != nil {
return database.CryptoKey{}, err
}
return q.db.DeleteCryptoKey(ctx, arg)
}
func (q *querier) DeleteCustomRole(ctx context.Context, arg database.DeleteCustomRoleParams) error {
if arg.OrganizationID.UUID != uuid.Nil {
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {
@@ -1383,6 +1391,20 @@ func (q *querier) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (stri
return q.db.GetCoordinatorResumeTokenSigningKey(ctx)
}
func (q *querier) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg database.GetCryptoKeyByFeatureAndSequenceParams) (database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil {
return database.CryptoKey{}, err
}
return q.db.GetCryptoKeyByFeatureAndSequence(ctx, arg)
}
func (q *querier) GetCryptoKeys(ctx context.Context) ([]database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil {
return nil, err
}
return q.db.GetCryptoKeys(ctx)
}
func (q *querier) GetDBCryptKeys(ctx context.Context) ([]database.DBCryptKey, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
return nil, err
@@ -1549,6 +1571,13 @@ func (q *querier) GetLastUpdateCheck(ctx context.Context) (string, error) {
return q.db.GetLastUpdateCheck(ctx)
}
func (q *querier) GetLatestCryptoKeyByFeature(ctx context.Context, feature database.CryptoKeyFeature) (database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil {
return database.CryptoKey{}, err
}
return q.db.GetLatestCryptoKeyByFeature(ctx, feature)
}
func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil {
return database.WorkspaceBuild{}, err
@@ -2662,6 +2691,13 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertAuditLog)(ctx, arg)
}
func (q *querier) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceCryptoKey); err != nil {
return database.CryptoKey{}, err
}
return q.db.InsertCryptoKey(ctx, arg)
}
func (q *querier) InsertCustomRole(ctx context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) {
// Org and site role upsert share the same query. So switch the assertion based on the org uuid.
if arg.OrganizationID.UUID != uuid.Nil {
@@ -3169,6 +3205,13 @@ func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKe
return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg)
}
func (q *querier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceCryptoKey); err != nil {
return database.CryptoKey{}, err
}
return q.db.UpdateCryptoKeyDeletesAt(ctx, arg)
}
func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) {
if arg.OrganizationID.UUID != uuid.Nil {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {
+51
View File
@@ -2253,6 +2253,57 @@ func (s *MethodTestSuite) TestDBCrypt() {
}))
}
func (s *MethodTestSuite) TestCryptoKeys() {
s.Run("GetCryptoKeys", s.Subtest(func(db database.Store, check *expects) {
check.Args().
Asserts(rbac.ResourceCryptoKey, policy.ActionRead)
}))
s.Run("InsertCryptoKey", s.Subtest(func(db database.Store, check *expects) {
check.Args(database.InsertCryptoKeyParams{
Feature: database.CryptoKeyFeatureWorkspaceApps,
}).
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,
Sequence: 4,
})
check.Args(database.DeleteCryptoKeyParams{
Feature: key.Feature,
Sequence: key.Sequence,
}).Asserts(rbac.ResourceCryptoKey, policy.ActionDelete)
}))
s.Run("GetCryptoKeyByFeatureAndSequence", s.Subtest(func(db database.Store, check *expects) {
key := dbgen.CryptoKey(s.T(), db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceApps,
Sequence: 4,
})
check.Args(database.GetCryptoKeyByFeatureAndSequenceParams{
Feature: key.Feature,
Sequence: key.Sequence,
}).Asserts(rbac.ResourceCryptoKey, policy.ActionRead).Returns(key)
}))
s.Run("GetLatestCryptoKeyByFeature", s.Subtest(func(db database.Store, check *expects) {
dbgen.CryptoKey(s.T(), db, database.CryptoKey{
Feature: database.CryptoKeyFeatureWorkspaceApps,
Sequence: 4,
})
check.Args(database.CryptoKeyFeatureWorkspaceApps).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,
Sequence: 4,
})
check.Args(database.UpdateCryptoKeyDeletesAtParams{
Feature: key.Feature,
Sequence: key.Sequence,
DeletesAt: sql.NullTime{Time: time.Now(), Valid: true},
}).Asserts(rbac.ResourceCryptoKey, policy.ActionUpdate)
}))
}
func (s *MethodTestSuite) TestSystemFunctions() {
s.Run("UpdateUserLinkedID", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
+57
View File
@@ -2,6 +2,7 @@ package dbgen
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
@@ -16,6 +17,7 @@ import (
"github.com/google/uuid"
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
@@ -893,6 +895,40 @@ func CustomRole(t testing.TB, db database.Store, seed database.CustomRole) datab
return role
}
func CryptoKey(t testing.TB, db database.Store, seed database.CryptoKey) database.CryptoKey {
t.Helper()
seed.Feature = takeFirst(seed.Feature, database.CryptoKeyFeatureWorkspaceApps)
if !seed.Secret.Valid {
secret, err := newCryptoKeySecret(seed.Feature)
require.NoError(t, err, "generate secret")
seed.Secret = sql.NullString{
String: secret,
Valid: true,
}
}
key, err := db.InsertCryptoKey(genCtx, database.InsertCryptoKeyParams{
Sequence: takeFirst(seed.Sequence, 123),
Secret: seed.Secret,
SecretKeyID: takeFirst(seed.SecretKeyID, sql.NullString{}),
Feature: seed.Feature,
StartsAt: takeFirst(seed.StartsAt, time.Now()),
})
require.NoError(t, err, "insert crypto key")
if seed.DeletesAt.Valid {
key, err = db.UpdateCryptoKeyDeletesAt(genCtx, database.UpdateCryptoKeyDeletesAtParams{
Feature: key.Feature,
Sequence: key.Sequence,
DeletesAt: sql.NullTime{Time: seed.DeletesAt.Time, Valid: true},
})
require.NoError(t, err, "update crypto key deletes_at")
}
return key
}
func ProvisionerJobTimings(t testing.TB, db database.Store, seed database.InsertProvisionerJobTimingsParams) []database.ProvisionerJobTiming {
timings, err := db.InsertProvisionerJobTimings(genCtx, seed)
require.NoError(t, err, "insert provisioner job timings")
@@ -942,3 +978,24 @@ func takeFirst[Value comparable](values ...Value) Value {
return v != empty
})
}
func newCryptoKeySecret(feature database.CryptoKeyFeature) (string, error) {
switch feature {
case database.CryptoKeyFeatureWorkspaceApps:
return generateCryptoKey(96)
case database.CryptoKeyFeatureOidcConvert:
return generateCryptoKey(32)
case database.CryptoKeyFeatureTailnetResume:
return generateCryptoKey(64)
}
return "", xerrors.Errorf("unknown feature: %s", feature)
}
func generateCryptoKey(length int) (string, error) {
b := make([]byte, length)
_, err := rand.Read(b)
if err != nil {
return "", xerrors.Errorf("rand read: %w", err)
}
return hex.EncodeToString(b), nil
}
+114
View File
@@ -189,6 +189,7 @@ type data struct {
// New tables
workspaceAgentStats []database.WorkspaceAgentStat
auditLogs []database.AuditLog
cryptoKeys []database.CryptoKey
dbcryptKeys []database.DBCryptKey
files []database.File
externalAuthLinks []database.ExternalAuthLink
@@ -1471,6 +1472,27 @@ func (*FakeQuerier) DeleteCoordinator(context.Context, uuid.UUID) error {
return ErrUnimplemented
}
func (q *FakeQuerier) DeleteCryptoKey(_ context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.CryptoKey{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, key := range q.cryptoKeys {
if key.Feature == arg.Feature && key.Sequence == arg.Sequence {
q.cryptoKeys[i].Secret.String = ""
q.cryptoKeys[i].Secret.Valid = false
q.cryptoKeys[i].SecretKeyID.String = ""
q.cryptoKeys[i].SecretKeyID.Valid = false
return q.cryptoKeys[i], nil
}
}
return database.CryptoKey{}, sql.ErrNoRows
}
func (q *FakeQuerier) DeleteCustomRole(_ context.Context, arg database.DeleteCustomRoleParams) error {
err := validateDatabaseType(arg)
if err != nil {
@@ -2346,6 +2368,41 @@ func (q *FakeQuerier) GetCoordinatorResumeTokenSigningKey(_ context.Context) (st
return q.coordinatorResumeTokenSigningKey, nil
}
func (q *FakeQuerier) GetCryptoKeyByFeatureAndSequence(_ context.Context, arg database.GetCryptoKeyByFeatureAndSequenceParams) (database.CryptoKey, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.CryptoKey{}, err
}
q.mutex.RLock()
defer q.mutex.RUnlock()
for _, key := range q.cryptoKeys {
if key.Feature == arg.Feature && key.Sequence == arg.Sequence {
// Keys with NULL secrets are considered deleted.
if key.Secret.Valid {
return key, nil
}
return database.CryptoKey{}, sql.ErrNoRows
}
}
return database.CryptoKey{}, sql.ErrNoRows
}
func (q *FakeQuerier) GetCryptoKeys(_ context.Context) ([]database.CryptoKey, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
keys := make([]database.CryptoKey, 0)
for _, key := range q.cryptoKeys {
if key.Secret.Valid {
keys = append(keys, key)
}
}
return keys, nil
}
func (q *FakeQuerier) GetDBCryptKeys(_ context.Context) ([]database.DBCryptKey, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -2849,6 +2906,22 @@ func (q *FakeQuerier) GetLastUpdateCheck(_ context.Context) (string, error) {
return string(q.lastUpdateCheck), nil
}
func (q *FakeQuerier) GetLatestCryptoKeyByFeature(_ context.Context, feature database.CryptoKeyFeature) (database.CryptoKey, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
var latestKey database.CryptoKey
for _, key := range q.cryptoKeys {
if key.Feature == feature && latestKey.Sequence < key.Sequence {
latestKey = key
}
}
if latestKey.StartsAt.IsZero() {
return database.CryptoKey{}, sql.ErrNoRows
}
return latestKey, nil
}
func (q *FakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -6368,6 +6441,28 @@ func (q *FakeQuerier) InsertAuditLog(_ context.Context, arg database.InsertAudit
return alog, nil
}
func (q *FakeQuerier) InsertCryptoKey(_ context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.CryptoKey{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
key := database.CryptoKey{
Feature: arg.Feature,
Sequence: arg.Sequence,
Secret: arg.Secret,
SecretKeyID: arg.SecretKeyID,
StartsAt: arg.StartsAt,
}
q.cryptoKeys = append(q.cryptoKeys, key)
return key, nil
}
func (q *FakeQuerier) InsertCustomRole(_ context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) {
err := validateDatabaseType(arg)
if err != nil {
@@ -7874,6 +7969,25 @@ func (q *FakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateCryptoKeyDeletesAt(_ context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) {
err := validateDatabaseType(arg)
if err != nil {
return database.CryptoKey{}, err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, key := range q.cryptoKeys {
if key.Feature == arg.Feature && key.Sequence == arg.Sequence {
key.DeletesAt = arg.DeletesAt
q.cryptoKeys[i] = key
return key, nil
}
}
return database.CryptoKey{}, sql.ErrNoRows
}
func (q *FakeQuerier) UpdateCustomRole(_ context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) {
err := validateDatabaseType(arg)
if err != nil {
+42
View File
@@ -214,6 +214,13 @@ func (m metricsStore) DeleteCoordinator(ctx context.Context, id uuid.UUID) error
return r0
}
func (m metricsStore) DeleteCryptoKey(ctx context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) {
start := time.Now()
r0, r1 := m.s.DeleteCryptoKey(ctx, arg)
m.queryLatencies.WithLabelValues("DeleteCryptoKey").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) DeleteCustomRole(ctx context.Context, arg database.DeleteCustomRoleParams) error {
start := time.Now()
r0 := m.s.DeleteCustomRole(ctx, arg)
@@ -543,6 +550,20 @@ func (m metricsStore) GetCoordinatorResumeTokenSigningKey(ctx context.Context) (
return r0, r1
}
func (m metricsStore) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg database.GetCryptoKeyByFeatureAndSequenceParams) (database.CryptoKey, error) {
start := time.Now()
r0, r1 := m.s.GetCryptoKeyByFeatureAndSequence(ctx, arg)
m.queryLatencies.WithLabelValues("GetCryptoKeyByFeatureAndSequence").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetCryptoKeys(ctx context.Context) ([]database.CryptoKey, error) {
start := time.Now()
r0, r1 := m.s.GetCryptoKeys(ctx)
m.queryLatencies.WithLabelValues("GetCryptoKeys").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetDBCryptKeys(ctx context.Context) ([]database.DBCryptKey, error) {
start := time.Now()
r0, r1 := m.s.GetDBCryptKeys(ctx)
@@ -711,6 +732,13 @@ func (m metricsStore) GetLastUpdateCheck(ctx context.Context) (string, error) {
return version, err
}
func (m metricsStore) GetLatestCryptoKeyByFeature(ctx context.Context, feature database.CryptoKeyFeature) (database.CryptoKey, error) {
start := time.Now()
r0, r1 := m.s.GetLatestCryptoKeyByFeature(ctx, feature)
m.queryLatencies.WithLabelValues("GetLatestCryptoKeyByFeature").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m metricsStore) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
start := time.Now()
build, err := m.s.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID)
@@ -1600,6 +1628,13 @@ func (m metricsStore) InsertAuditLog(ctx context.Context, arg database.InsertAud
return log, err
}
func (m metricsStore) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
start := time.Now()
key, err := m.s.InsertCryptoKey(ctx, arg)
m.queryLatencies.WithLabelValues("InsertCryptoKey").Observe(time.Since(start).Seconds())
return key, err
}
func (m metricsStore) InsertCustomRole(ctx context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) {
start := time.Now()
r0, r1 := m.s.InsertCustomRole(ctx, arg)
@@ -2006,6 +2041,13 @@ func (m metricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateA
return err
}
func (m metricsStore) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) {
start := time.Now()
key, err := m.s.UpdateCryptoKeyDeletesAt(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateCryptoKeyDeletesAt").Observe(time.Since(start).Seconds())
return key, err
}
func (m metricsStore) UpdateCustomRole(ctx context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) {
start := time.Now()
r0, r1 := m.s.UpdateCustomRole(ctx, arg)
+90
View File
@@ -317,6 +317,21 @@ func (mr *MockStoreMockRecorder) DeleteCoordinator(arg0, arg1 any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCoordinator", reflect.TypeOf((*MockStore)(nil).DeleteCoordinator), arg0, arg1)
}
// DeleteCryptoKey mocks base method.
func (m *MockStore) DeleteCryptoKey(arg0 context.Context, arg1 database.DeleteCryptoKeyParams) (database.CryptoKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteCryptoKey", arg0, arg1)
ret0, _ := ret[0].(database.CryptoKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteCryptoKey indicates an expected call of DeleteCryptoKey.
func (mr *MockStoreMockRecorder) DeleteCryptoKey(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCryptoKey", reflect.TypeOf((*MockStore)(nil).DeleteCryptoKey), arg0, arg1)
}
// DeleteCustomRole mocks base method.
func (m *MockStore) DeleteCustomRole(arg0 context.Context, arg1 database.DeleteCustomRoleParams) error {
m.ctrl.T.Helper()
@@ -1058,6 +1073,36 @@ func (mr *MockStoreMockRecorder) GetCoordinatorResumeTokenSigningKey(arg0 any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCoordinatorResumeTokenSigningKey", reflect.TypeOf((*MockStore)(nil).GetCoordinatorResumeTokenSigningKey), arg0)
}
// GetCryptoKeyByFeatureAndSequence mocks base method.
func (m *MockStore) GetCryptoKeyByFeatureAndSequence(arg0 context.Context, arg1 database.GetCryptoKeyByFeatureAndSequenceParams) (database.CryptoKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCryptoKeyByFeatureAndSequence", arg0, arg1)
ret0, _ := ret[0].(database.CryptoKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCryptoKeyByFeatureAndSequence indicates an expected call of GetCryptoKeyByFeatureAndSequence.
func (mr *MockStoreMockRecorder) GetCryptoKeyByFeatureAndSequence(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCryptoKeyByFeatureAndSequence", reflect.TypeOf((*MockStore)(nil).GetCryptoKeyByFeatureAndSequence), arg0, arg1)
}
// GetCryptoKeys mocks base method.
func (m *MockStore) GetCryptoKeys(arg0 context.Context) ([]database.CryptoKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCryptoKeys", arg0)
ret0, _ := ret[0].([]database.CryptoKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCryptoKeys indicates an expected call of GetCryptoKeys.
func (mr *MockStoreMockRecorder) GetCryptoKeys(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCryptoKeys", reflect.TypeOf((*MockStore)(nil).GetCryptoKeys), arg0)
}
// GetDBCryptKeys mocks base method.
func (m *MockStore) GetDBCryptKeys(arg0 context.Context) ([]database.DBCryptKey, error) {
m.ctrl.T.Helper()
@@ -1418,6 +1463,21 @@ func (mr *MockStoreMockRecorder) GetLastUpdateCheck(arg0 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastUpdateCheck", reflect.TypeOf((*MockStore)(nil).GetLastUpdateCheck), arg0)
}
// GetLatestCryptoKeyByFeature mocks base method.
func (m *MockStore) GetLatestCryptoKeyByFeature(arg0 context.Context, arg1 database.CryptoKeyFeature) (database.CryptoKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLatestCryptoKeyByFeature", arg0, arg1)
ret0, _ := ret[0].(database.CryptoKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLatestCryptoKeyByFeature indicates an expected call of GetLatestCryptoKeyByFeature.
func (mr *MockStoreMockRecorder) GetLatestCryptoKeyByFeature(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestCryptoKeyByFeature", reflect.TypeOf((*MockStore)(nil).GetLatestCryptoKeyByFeature), arg0, arg1)
}
// GetLatestWorkspaceBuildByWorkspaceID mocks base method.
func (m *MockStore) GetLatestWorkspaceBuildByWorkspaceID(arg0 context.Context, arg1 uuid.UUID) (database.WorkspaceBuild, error) {
m.ctrl.T.Helper()
@@ -3367,6 +3427,21 @@ func (mr *MockStoreMockRecorder) InsertAuditLog(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertAuditLog", reflect.TypeOf((*MockStore)(nil).InsertAuditLog), arg0, arg1)
}
// InsertCryptoKey mocks base method.
func (m *MockStore) InsertCryptoKey(arg0 context.Context, arg1 database.InsertCryptoKeyParams) (database.CryptoKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertCryptoKey", arg0, arg1)
ret0, _ := ret[0].(database.CryptoKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertCryptoKey indicates an expected call of InsertCryptoKey.
func (mr *MockStoreMockRecorder) InsertCryptoKey(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertCryptoKey", reflect.TypeOf((*MockStore)(nil).InsertCryptoKey), arg0, arg1)
}
// InsertCustomRole mocks base method.
func (m *MockStore) InsertCustomRole(arg0 context.Context, arg1 database.InsertCustomRoleParams) (database.CustomRole, error) {
m.ctrl.T.Helper()
@@ -4234,6 +4309,21 @@ func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAPIKeyByID", reflect.TypeOf((*MockStore)(nil).UpdateAPIKeyByID), arg0, arg1)
}
// UpdateCryptoKeyDeletesAt mocks base method.
func (m *MockStore) UpdateCryptoKeyDeletesAt(arg0 context.Context, arg1 database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateCryptoKeyDeletesAt", arg0, arg1)
ret0, _ := ret[0].(database.CryptoKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateCryptoKeyDeletesAt indicates an expected call of UpdateCryptoKeyDeletesAt.
func (mr *MockStoreMockRecorder) UpdateCryptoKeyDeletesAt(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateCryptoKeyDeletesAt", reflect.TypeOf((*MockStore)(nil).UpdateCryptoKeyDeletesAt), arg0, arg1)
}
// UpdateCustomRole mocks base method.
func (m *MockStore) UpdateCustomRole(arg0 context.Context, arg1 database.UpdateCustomRoleParams) (database.CustomRole, error) {
m.ctrl.T.Helper()
+21
View File
@@ -36,6 +36,12 @@ CREATE TYPE build_reason AS ENUM (
'autodelete'
);
CREATE TYPE crypto_key_feature AS ENUM (
'workspace_apps',
'oidc_convert',
'tailnet_resume'
);
CREATE TYPE display_app AS ENUM (
'vscode',
'vscode_insiders',
@@ -494,6 +500,15 @@ CREATE TABLE audit_logs (
resource_icon text NOT NULL
);
CREATE TABLE crypto_keys (
feature crypto_key_feature NOT NULL,
sequence integer NOT NULL,
secret text,
secret_key_id text,
starts_at timestamp with time zone NOT NULL,
deletes_at timestamp with time zone
);
CREATE TABLE custom_roles (
name text NOT NULL,
display_name text NOT NULL,
@@ -1641,6 +1656,9 @@ ALTER TABLE ONLY api_keys
ALTER TABLE ONLY audit_logs
ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
ALTER TABLE ONLY crypto_keys
ADD CONSTRAINT crypto_keys_pkey PRIMARY KEY (feature, sequence);
ALTER TABLE ONLY custom_roles
ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id);
@@ -2036,6 +2054,9 @@ CREATE TRIGGER update_notification_message_dedupe_hash BEFORE INSERT OR UPDATE O
ALTER TABLE ONLY api_keys
ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY crypto_keys
ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ALTER TABLE ONLY external_auth_links
ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
@@ -7,6 +7,7 @@ type ForeignKeyConstraint string
// ForeignKeyConstraint enums.
const (
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyCryptoKeysSecretKeyID ForeignKeyConstraint = "crypto_keys_secret_key_id_fkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_secret_key_id_fkey FOREIGN KEY (secret_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
@@ -0,0 +1,2 @@
DROP TABLE "crypto_keys";
DROP TYPE "crypto_key_feature";
@@ -0,0 +1,16 @@
CREATE TYPE crypto_key_feature AS ENUM (
'workspace_apps',
'oidc_convert',
'tailnet_resume'
);
CREATE TABLE crypto_keys (
feature crypto_key_feature NOT NULL,
sequence integer NOT NULL,
secret text NULL,
secret_key_id text NULL REFERENCES dbcrypt_keys(active_key_digest),
starts_at timestamptz NOT NULL,
deletes_at timestamptz NULL,
PRIMARY KEY (feature, sequence)
);
@@ -0,0 +1,4 @@
INSERT INTO crypto_keys (feature, sequence, starts_at, secret) VALUES
('workspace_apps', 1, now(), 'abc'),
('oidc_convert', 1, now(), 'def'),
('tailnet_resume', 1, now(), 'ghi');
+4
View File
@@ -447,3 +447,7 @@ func (r GetAuthorizationUserRolesRow) RoleNames() ([]rbac.RoleIdentifier, error)
}
return names, nil
}
func (k CryptoKey) ExpiresAt(keyDuration time.Duration) time.Time {
return k.StartsAt.Add(keyDuration).UTC()
}
+70
View File
@@ -339,6 +339,67 @@ func AllBuildReasonValues() []BuildReason {
}
}
type CryptoKeyFeature string
const (
CryptoKeyFeatureWorkspaceApps CryptoKeyFeature = "workspace_apps"
CryptoKeyFeatureOidcConvert CryptoKeyFeature = "oidc_convert"
CryptoKeyFeatureTailnetResume CryptoKeyFeature = "tailnet_resume"
)
func (e *CryptoKeyFeature) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = CryptoKeyFeature(s)
case string:
*e = CryptoKeyFeature(s)
default:
return fmt.Errorf("unsupported scan type for CryptoKeyFeature: %T", src)
}
return nil
}
type NullCryptoKeyFeature struct {
CryptoKeyFeature CryptoKeyFeature `json:"crypto_key_feature"`
Valid bool `json:"valid"` // Valid is true if CryptoKeyFeature is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullCryptoKeyFeature) Scan(value interface{}) error {
if value == nil {
ns.CryptoKeyFeature, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.CryptoKeyFeature.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullCryptoKeyFeature) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.CryptoKeyFeature), nil
}
func (e CryptoKeyFeature) Valid() bool {
switch e {
case CryptoKeyFeatureWorkspaceApps,
CryptoKeyFeatureOidcConvert,
CryptoKeyFeatureTailnetResume:
return true
}
return false
}
func AllCryptoKeyFeatureValues() []CryptoKeyFeature {
return []CryptoKeyFeature{
CryptoKeyFeatureWorkspaceApps,
CryptoKeyFeatureOidcConvert,
CryptoKeyFeatureTailnetResume,
}
}
type DisplayApp string
const (
@@ -2043,6 +2104,15 @@ type AuditLog struct {
ResourceIcon string `db:"resource_icon" json:"resource_icon"`
}
type CryptoKey struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
Secret sql.NullString `db:"secret" json:"secret"`
SecretKeyID sql.NullString `db:"secret_key_id" json:"secret_key_id"`
StartsAt time.Time `db:"starts_at" json:"starts_at"`
DeletesAt sql.NullTime `db:"deletes_at" json:"deletes_at"`
}
// Custom roles allow dynamic roles expanded at runtime
type CustomRole struct {
Name string `db:"name" json:"name"`
+6
View File
@@ -69,6 +69,7 @@ type sqlcQuerier interface {
DeleteAllTailnetTunnels(ctx context.Context, arg DeleteAllTailnetTunnelsParams) error
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
DeleteCoordinator(ctx context.Context, id uuid.UUID) error
DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error)
DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error
DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error
DeleteGitSSHKey(ctx context.Context, userID uuid.UUID) error
@@ -131,6 +132,8 @@ type sqlcQuerier interface {
// are included.
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
GetCoordinatorResumeTokenSigningKey(ctx context.Context) (string, error)
GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg GetCryptoKeyByFeatureAndSequenceParams) (CryptoKey, error)
GetCryptoKeys(ctx context.Context) ([]CryptoKey, error)
GetDBCryptKeys(ctx context.Context) ([]DBCryptKey, error)
GetDERPMeshKey(ctx context.Context) (string, error)
GetDefaultOrganization(ctx context.Context) (Organization, error)
@@ -159,6 +162,7 @@ type sqlcQuerier interface {
GetHungProvisionerJobs(ctx context.Context, updatedAt time.Time) ([]ProvisionerJob, error)
GetJFrogXrayScanByWorkspaceAndAgentID(ctx context.Context, arg GetJFrogXrayScanByWorkspaceAndAgentIDParams) (JfrogXrayScan, error)
GetLastUpdateCheck(ctx context.Context) (string, error)
GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error)
GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (WorkspaceBuild, error)
GetLatestWorkspaceBuilds(ctx context.Context) ([]WorkspaceBuild, error)
GetLatestWorkspaceBuildsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceBuild, error)
@@ -338,6 +342,7 @@ type sqlcQuerier interface {
// every member of the org.
InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error)
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
InsertCryptoKey(ctx context.Context, arg InsertCryptoKeyParams) (CryptoKey, error)
InsertCustomRole(ctx context.Context, arg InsertCustomRoleParams) (CustomRole, error)
InsertDBCryptKey(ctx context.Context, arg InsertDBCryptKeyParams) error
InsertDERPMeshKey(ctx context.Context, value string) error
@@ -412,6 +417,7 @@ type sqlcQuerier interface {
UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error
UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error)
UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error)
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
UpdateGitSSHKey(ctx context.Context, arg UpdateGitSSHKeyParams) (GitSSHKey, error)
+180
View File
@@ -761,6 +761,186 @@ func (q *sqlQuerier) InsertAuditLog(ctx context.Context, arg InsertAuditLogParam
return i, err
}
const deleteCryptoKey = `-- name: DeleteCryptoKey :one
UPDATE crypto_keys
SET secret = NULL, secret_key_id = NULL
WHERE feature = $1 AND sequence = $2 RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`
type DeleteCryptoKeyParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
}
func (q *sqlQuerier) DeleteCryptoKey(ctx context.Context, arg DeleteCryptoKeyParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, deleteCryptoKey, arg.Feature, arg.Sequence)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const getCryptoKeyByFeatureAndSequence = `-- name: GetCryptoKeyByFeatureAndSequence :one
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
AND sequence = $2
AND secret IS NOT NULL
`
type GetCryptoKeyByFeatureAndSequenceParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
}
func (q *sqlQuerier) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg GetCryptoKeyByFeatureAndSequenceParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, getCryptoKeyByFeatureAndSequence, arg.Feature, arg.Sequence)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const getCryptoKeys = `-- name: GetCryptoKeys :many
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE secret IS NOT NULL
`
func (q *sqlQuerier) GetCryptoKeys(ctx context.Context) ([]CryptoKey, error) {
rows, err := q.db.QueryContext(ctx, getCryptoKeys)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CryptoKey
for rows.Next() {
var i CryptoKey
if err := rows.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getLatestCryptoKeyByFeature = `-- name: GetLatestCryptoKeyByFeature :one
SELECT feature, sequence, secret, secret_key_id, starts_at, deletes_at
FROM crypto_keys
WHERE feature = $1
ORDER BY sequence DESC
LIMIT 1
`
func (q *sqlQuerier) GetLatestCryptoKeyByFeature(ctx context.Context, feature CryptoKeyFeature) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, getLatestCryptoKeyByFeature, feature)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const insertCryptoKey = `-- name: InsertCryptoKey :one
INSERT INTO crypto_keys (
feature,
sequence,
secret,
starts_at,
secret_key_id
) VALUES (
$1,
$2,
$3,
$4,
$5
) RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`
type InsertCryptoKeyParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
Secret sql.NullString `db:"secret" json:"secret"`
StartsAt time.Time `db:"starts_at" json:"starts_at"`
SecretKeyID sql.NullString `db:"secret_key_id" json:"secret_key_id"`
}
func (q *sqlQuerier) InsertCryptoKey(ctx context.Context, arg InsertCryptoKeyParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, insertCryptoKey,
arg.Feature,
arg.Sequence,
arg.Secret,
arg.StartsAt,
arg.SecretKeyID,
)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const updateCryptoKeyDeletesAt = `-- name: UpdateCryptoKeyDeletesAt :one
UPDATE crypto_keys
SET deletes_at = $3
WHERE feature = $1 AND sequence = $2 RETURNING feature, sequence, secret, secret_key_id, starts_at, deletes_at
`
type UpdateCryptoKeyDeletesAtParams struct {
Feature CryptoKeyFeature `db:"feature" json:"feature"`
Sequence int32 `db:"sequence" json:"sequence"`
DeletesAt sql.NullTime `db:"deletes_at" json:"deletes_at"`
}
func (q *sqlQuerier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error) {
row := q.db.QueryRowContext(ctx, updateCryptoKeyDeletesAt, arg.Feature, arg.Sequence, arg.DeletesAt)
var i CryptoKey
err := row.Scan(
&i.Feature,
&i.Sequence,
&i.Secret,
&i.SecretKeyID,
&i.StartsAt,
&i.DeletesAt,
)
return i, err
}
const getDBCryptKeys = `-- name: GetDBCryptKeys :many
SELECT number, active_key_digest, revoked_key_digest, created_at, revoked_at, test FROM dbcrypt_keys ORDER BY number ASC
`
+43
View File
@@ -0,0 +1,43 @@
-- name: GetCryptoKeys :many
SELECT *
FROM crypto_keys
WHERE secret IS NOT NULL;
-- name: GetLatestCryptoKeyByFeature :one
SELECT *
FROM crypto_keys
WHERE feature = $1
ORDER BY sequence DESC
LIMIT 1;
-- name: GetCryptoKeyByFeatureAndSequence :one
SELECT *
FROM crypto_keys
WHERE feature = $1
AND sequence = $2
AND secret IS NOT NULL;
-- name: DeleteCryptoKey :one
UPDATE crypto_keys
SET secret = NULL, secret_key_id = NULL
WHERE feature = $1 AND sequence = $2 RETURNING *;
-- name: InsertCryptoKey :one
INSERT INTO crypto_keys (
feature,
sequence,
secret,
starts_at,
secret_key_id
) VALUES (
$1,
$2,
$3,
$4,
$5
) RETURNING *;
-- name: UpdateCryptoKeyDeletesAt :one
UPDATE crypto_keys
SET deletes_at = $3
WHERE feature = $1 AND sequence = $2 RETURNING *;
+1
View File
@@ -9,6 +9,7 @@ const (
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
UniqueCryptoKeysPkey UniqueConstraint = "crypto_keys_pkey" // ALTER TABLE ONLY crypto_keys ADD CONSTRAINT crypto_keys_pkey PRIMARY KEY (feature, sequence);
UniqueCustomRolesUniqueKey UniqueConstraint = "custom_roles_unique_key" // ALTER TABLE ONLY custom_roles ADD CONSTRAINT custom_roles_unique_key UNIQUE (name, organization_id);
UniqueDbcryptKeysActiveKeyDigestKey UniqueConstraint = "dbcrypt_keys_active_key_digest_key" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_active_key_digest_key UNIQUE (active_key_digest);
UniqueDbcryptKeysPkey UniqueConstraint = "dbcrypt_keys_pkey" // ALTER TABLE ONLY dbcrypt_keys ADD CONSTRAINT dbcrypt_keys_pkey PRIMARY KEY (number);
+11
View File
@@ -55,6 +55,16 @@ var (
Type: "audit_log",
}
// ResourceCryptoKey
// Valid Actions
// - "ActionCreate" :: create crypto keys
// - "ActionDelete" :: delete crypto keys
// - "ActionRead" :: read crypto keys
// - "ActionUpdate" :: update crypto keys
ResourceCryptoKey = Object{
Type: "crypto_key",
}
// ResourceDebugInfo
// Valid Actions
// - "ActionRead" :: access to debug routes
@@ -299,6 +309,7 @@ func AllResources() []Objecter {
ResourceAssignOrgRole,
ResourceAssignRole,
ResourceAuditLog,
ResourceCryptoKey,
ResourceDebugInfo,
ResourceDeploymentConfig,
ResourceDeploymentStats,
+8
View File
@@ -274,6 +274,14 @@ var RBACPermissions = map[string]PermissionDefinition{
ActionUpdate: actDef("update notification preferences"),
},
},
"crypto_key": {
Actions: map[Action]ActionDefinition{
ActionRead: actDef("read crypto keys"),
ActionUpdate: actDef("update crypto keys"),
ActionDelete: actDef("delete crypto keys"),
ActionCreate: actDef("create crypto keys"),
},
},
// idpsync_settings should always be org scoped
"idpsync_settings": {
Actions: map[Action]ActionDefinition{
+9
View File
@@ -705,6 +705,15 @@ func TestRolePermissions(t *testing.T) {
},
},
},
{
Name: "CryptoKeys",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete, policy.ActionRead},
Resource: rbac.ResourceCryptoKey,
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner},
false: {setOtherOrg, setOrgNotMe, memberMe, orgMemberMe, templateAdmin, userAdmin},
},
},
{
Name: "IDPSyncSettings",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
+2
View File
@@ -9,6 +9,7 @@ const (
ResourceAssignOrgRole RBACResource = "assign_org_role"
ResourceAssignRole RBACResource = "assign_role"
ResourceAuditLog RBACResource = "audit_log"
ResourceCryptoKey RBACResource = "crypto_key"
ResourceDebugInfo RBACResource = "debug_info"
ResourceDeploymentConfig RBACResource = "deployment_config"
ResourceDeploymentStats RBACResource = "deployment_stats"
@@ -62,6 +63,7 @@ var RBACResourceActions = map[RBACResource][]RBACAction{
ResourceAssignOrgRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceAssignRole: {ActionAssign, ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceAuditLog: {ActionCreate, ActionRead},
ResourceCryptoKey: {ActionCreate, ActionDelete, ActionRead, ActionUpdate},
ResourceDebugInfo: {ActionRead},
ResourceDeploymentConfig: {ActionRead, ActionUpdate},
ResourceDeploymentStats: {ActionRead},
+5
View File
@@ -184,6 +184,7 @@ Status Code **200**
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
| `resource_type` | `audit_log` |
| `resource_type` | `crypto_key` |
| `resource_type` | `debug_info` |
| `resource_type` | `deployment_config` |
| `resource_type` | `deployment_stats` |
@@ -343,6 +344,7 @@ Status Code **200**
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
| `resource_type` | `audit_log` |
| `resource_type` | `crypto_key` |
| `resource_type` | `debug_info` |
| `resource_type` | `deployment_config` |
| `resource_type` | `deployment_stats` |
@@ -502,6 +504,7 @@ Status Code **200**
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
| `resource_type` | `audit_log` |
| `resource_type` | `crypto_key` |
| `resource_type` | `debug_info` |
| `resource_type` | `deployment_config` |
| `resource_type` | `deployment_stats` |
@@ -630,6 +633,7 @@ Status Code **200**
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
| `resource_type` | `audit_log` |
| `resource_type` | `crypto_key` |
| `resource_type` | `debug_info` |
| `resource_type` | `deployment_config` |
| `resource_type` | `deployment_stats` |
@@ -888,6 +892,7 @@ Status Code **200**
| `resource_type` | `assign_org_role` |
| `resource_type` | `assign_role` |
| `resource_type` | `audit_log` |
| `resource_type` | `crypto_key` |
| `resource_type` | `debug_info` |
| `resource_type` | `deployment_config` |
| `resource_type` | `deployment_stats` |
+1
View File
@@ -4384,6 +4384,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
| `assign_org_role` |
| `assign_role` |
| `audit_log` |
| `crypto_key` |
| `debug_info` |
| `deployment_config` |
| `deployment_stats` |
+60
View File
@@ -261,6 +261,66 @@ func (db *dbCrypt) UpdateExternalAuthLink(ctx context.Context, params database.U
return link, nil
}
func (db *dbCrypt) GetCryptoKeys(ctx context.Context) ([]database.CryptoKey, error) {
keys, err := db.Store.GetCryptoKeys(ctx)
if err != nil {
return nil, err
}
for i := range keys {
if err := db.decryptField(&keys[i].Secret.String, keys[i].SecretKeyID); err != nil {
return nil, err
}
}
return keys, nil
}
func (db *dbCrypt) GetLatestCryptoKeyByFeature(ctx context.Context, feature database.CryptoKeyFeature) (database.CryptoKey, error) {
key, err := db.Store.GetLatestCryptoKeyByFeature(ctx, feature)
if err != nil {
return database.CryptoKey{}, err
}
if err := db.decryptField(&key.Secret.String, key.SecretKeyID); err != nil {
return database.CryptoKey{}, err
}
return key, nil
}
func (db *dbCrypt) GetCryptoKeyByFeatureAndSequence(ctx context.Context, params database.GetCryptoKeyByFeatureAndSequenceParams) (database.CryptoKey, error) {
key, err := db.Store.GetCryptoKeyByFeatureAndSequence(ctx, params)
if err != nil {
return database.CryptoKey{}, err
}
if err := db.decryptField(&key.Secret.String, key.SecretKeyID); err != nil {
return database.CryptoKey{}, err
}
return key, nil
}
func (db *dbCrypt) InsertCryptoKey(ctx context.Context, params database.InsertCryptoKeyParams) (database.CryptoKey, error) {
if err := db.encryptField(&params.Secret.String, &params.SecretKeyID); err != nil {
return database.CryptoKey{}, err
}
key, err := db.Store.InsertCryptoKey(ctx, params)
if err != nil {
return database.CryptoKey{}, err
}
if err := db.decryptField(&key.Secret.String, key.SecretKeyID); err != nil {
return database.CryptoKey{}, err
}
return key, nil
}
func (db *dbCrypt) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) {
key, err := db.Store.UpdateCryptoKeyDeletesAt(ctx, arg)
if err != nil {
return database.CryptoKey{}, err
}
if err := db.decryptField(&key.Secret.String, key.SecretKeyID); err != nil {
return database.CryptoKey{}, err
}
return key, nil
}
func (db *dbCrypt) encryptField(field *string, digest *sql.NullString) error {
// If no cipher is loaded, then we can't encrypt anything!
if db.ciphers == nil || db.primaryCipherDigest == "" {
+124
View File
@@ -8,6 +8,7 @@ import (
"encoding/json"
"io"
"testing"
"time"
"github.com/lib/pq"
"github.com/stretchr/testify/require"
@@ -349,6 +350,129 @@ func TestExternalAuthLinks(t *testing.T) {
})
}
func TestCryptoKeys(t *testing.T) {
t.Parallel()
ctx := context.Background()
t.Run("InsertCryptoKey", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
key := dbgen.CryptoKey(t, crypt, database.CryptoKey{
Secret: sql.NullString{String: "test", Valid: true},
})
require.Equal(t, "test", key.Secret.String)
key, err := db.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
Feature: key.Feature,
Sequence: key.Sequence,
})
require.NoError(t, err)
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
requireEncryptedEquals(t, ciphers[0], key.Secret.String, "test")
})
t.Run("GetCryptoKeys", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
_ = dbgen.CryptoKey(t, crypt, database.CryptoKey{
Secret: sql.NullString{String: "test", Valid: true},
})
keys, err := crypt.GetCryptoKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
require.Equal(t, "test", keys[0].Secret.String)
require.Equal(t, ciphers[0].HexDigest(), keys[0].SecretKeyID.String)
keys, err = db.GetCryptoKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
requireEncryptedEquals(t, ciphers[0], keys[0].Secret.String, "test")
require.Equal(t, ciphers[0].HexDigest(), keys[0].SecretKeyID.String)
})
t.Run("GetLatestCryptoKeyByFeature", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
_ = dbgen.CryptoKey(t, crypt, database.CryptoKey{
Secret: sql.NullString{String: "test", Valid: true},
})
key, err := crypt.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceApps)
require.NoError(t, err)
require.Equal(t, "test", key.Secret.String)
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
key, err = db.GetLatestCryptoKeyByFeature(ctx, database.CryptoKeyFeatureWorkspaceApps)
require.NoError(t, err)
requireEncryptedEquals(t, ciphers[0], key.Secret.String, "test")
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
})
t.Run("GetCryptoKeyByFeatureAndSequence", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
key := dbgen.CryptoKey(t, crypt, database.CryptoKey{
Secret: sql.NullString{String: "test", Valid: true},
})
key, err := crypt.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
Feature: database.CryptoKeyFeatureWorkspaceApps,
Sequence: key.Sequence,
})
require.NoError(t, err)
require.Equal(t, "test", key.Secret.String)
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
key, err = db.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
Feature: database.CryptoKeyFeatureWorkspaceApps,
Sequence: key.Sequence,
})
require.NoError(t, err)
requireEncryptedEquals(t, ciphers[0], key.Secret.String, "test")
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
})
t.Run("UpdateCryptoKeyDeletesAt", func(t *testing.T) {
t.Parallel()
_, crypt, ciphers := setup(t)
key := dbgen.CryptoKey(t, crypt, database.CryptoKey{
Secret: sql.NullString{String: "test", Valid: true},
})
key, err := crypt.UpdateCryptoKeyDeletesAt(ctx, database.UpdateCryptoKeyDeletesAtParams{
Feature: key.Feature,
Sequence: key.Sequence,
DeletesAt: sql.NullTime{
Time: time.Now().Add(time.Hour),
Valid: true,
},
})
require.NoError(t, err)
require.Equal(t, "test", key.Secret.String)
require.Equal(t, ciphers[0].HexDigest(), key.SecretKeyID.String)
})
t.Run("DecryptErr", func(t *testing.T) {
t.Parallel()
db, crypt, ciphers := setup(t)
key := dbgen.CryptoKey(t, db, database.CryptoKey{
Secret: sql.NullString{
String: fakeBase64RandomData(t, 32),
Valid: true,
},
SecretKeyID: sql.NullString{
String: ciphers[0].HexDigest(),
Valid: true,
},
})
_, err := crypt.GetCryptoKeyByFeatureAndSequence(ctx, database.GetCryptoKeyByFeatureAndSequenceParams{
Feature: key.Feature,
Sequence: key.Sequence,
})
require.Error(t, err, "expected an error")
var derr *DecryptFailedError
require.ErrorAs(t, err, &derr, "expected a decrypt error")
})
}
func TestNew(t *testing.T) {
t.Parallel()
+6
View File
@@ -32,6 +32,12 @@ export const RBACResourceActions: Partial<
create: "create new audit log entries",
read: "read audit logs",
},
crypto_key: {
create: "create crypto keys",
delete: "delete crypto keys",
read: "read crypto keys",
update: "update crypto keys",
},
debug_info: {
read: "access to debug routes",
},
+2 -2
View File
@@ -2155,8 +2155,8 @@ export type RBACAction = "application_connect" | "assign" | "create" | "delete"
export const RBACActions: RBACAction[] = ["application_connect", "assign", "create", "delete", "read", "read_personal", "ssh", "start", "stop", "update", "update_personal", "use", "view_insights"]
// From codersdk/rbacresources_gen.go
export type RBACResource = "*" | "api_key" | "assign_org_role" | "assign_role" | "audit_log" | "debug_info" | "deployment_config" | "deployment_stats" | "file" | "group" | "group_member" | "idpsync_settings" | "license" | "notification_preference" | "notification_template" | "oauth2_app" | "oauth2_app_code_token" | "oauth2_app_secret" | "organization" | "organization_member" | "provisioner_daemon" | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" | "template" | "user" | "workspace" | "workspace_dormant" | "workspace_proxy"
export const RBACResources: RBACResource[] = ["*", "api_key", "assign_org_role", "assign_role", "audit_log", "debug_info", "deployment_config", "deployment_stats", "file", "group", "group_member", "idpsync_settings", "license", "notification_preference", "notification_template", "oauth2_app", "oauth2_app_code_token", "oauth2_app_secret", "organization", "organization_member", "provisioner_daemon", "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", "workspace_dormant", "workspace_proxy"]
export type RBACResource = "*" | "api_key" | "assign_org_role" | "assign_role" | "audit_log" | "crypto_key" | "debug_info" | "deployment_config" | "deployment_stats" | "file" | "group" | "group_member" | "idpsync_settings" | "license" | "notification_preference" | "notification_template" | "oauth2_app" | "oauth2_app_code_token" | "oauth2_app_secret" | "organization" | "organization_member" | "provisioner_daemon" | "provisioner_keys" | "replicas" | "system" | "tailnet_coordinator" | "template" | "user" | "workspace" | "workspace_dormant" | "workspace_proxy"
export const RBACResources: RBACResource[] = ["*", "api_key", "assign_org_role", "assign_role", "audit_log", "crypto_key", "debug_info", "deployment_config", "deployment_stats", "file", "group", "group_member", "idpsync_settings", "license", "notification_preference", "notification_template", "oauth2_app", "oauth2_app_code_token", "oauth2_app_secret", "organization", "organization_member", "provisioner_daemon", "provisioner_keys", "replicas", "system", "tailnet_coordinator", "template", "user", "workspace", "workspace_dormant", "workspace_proxy"]
// From codersdk/audit.go
export type ResourceType = "api_key" | "convert_login" | "custom_role" | "git_ssh_key" | "group" | "health_settings" | "license" | "notifications_settings" | "oauth2_provider_app" | "oauth2_provider_app_secret" | "organization" | "template" | "template_version" | "user" | "workspace" | "workspace_build" | "workspace_proxy"