Skip to content

Commit 45160c7

Browse files
authored
feat: add schema for key rotation (coder#14662)
1 parent 45420b9 commit 45160c7

29 files changed

+977
-2
lines changed

coderd/apidoc/docs.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/dbauthz/dbauthz.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ var (
253253
rbac.ResourceDeploymentConfig.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
254254
rbac.ResourceNotificationPreference.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
255255
rbac.ResourceNotificationTemplate.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
256+
rbac.ResourceCryptoKey.Type: {policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
256257
}),
257258
Org: map[string][]rbac.Permission{},
258259
User: []rbac.Permission{},
@@ -1041,6 +1042,13 @@ func (q *querier) DeleteCoordinator(ctx context.Context, id uuid.UUID) error {
10411042
return q.db.DeleteCoordinator(ctx, id)
10421043
}
10431044

1045+
func (q *querier) DeleteCryptoKey(ctx context.Context, arg database.DeleteCryptoKeyParams) (database.CryptoKey, error) {
1046+
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceCryptoKey); err != nil {
1047+
return database.CryptoKey{}, err
1048+
}
1049+
return q.db.DeleteCryptoKey(ctx, arg)
1050+
}
1051+
10441052
func (q *querier) DeleteCustomRole(ctx context.Context, arg database.DeleteCustomRoleParams) error {
10451053
if arg.OrganizationID.UUID != uuid.Nil {
10461054
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
13831391
return q.db.GetCoordinatorResumeTokenSigningKey(ctx)
13841392
}
13851393

1394+
func (q *querier) GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg database.GetCryptoKeyByFeatureAndSequenceParams) (database.CryptoKey, error) {
1395+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil {
1396+
return database.CryptoKey{}, err
1397+
}
1398+
return q.db.GetCryptoKeyByFeatureAndSequence(ctx, arg)
1399+
}
1400+
1401+
func (q *querier) GetCryptoKeys(ctx context.Context) ([]database.CryptoKey, error) {
1402+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil {
1403+
return nil, err
1404+
}
1405+
return q.db.GetCryptoKeys(ctx)
1406+
}
1407+
13861408
func (q *querier) GetDBCryptKeys(ctx context.Context) ([]database.DBCryptKey, error) {
13871409
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
13881410
return nil, err
@@ -1549,6 +1571,13 @@ func (q *querier) GetLastUpdateCheck(ctx context.Context) (string, error) {
15491571
return q.db.GetLastUpdateCheck(ctx)
15501572
}
15511573

1574+
func (q *querier) GetLatestCryptoKeyByFeature(ctx context.Context, feature database.CryptoKeyFeature) (database.CryptoKey, error) {
1575+
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceCryptoKey); err != nil {
1576+
return database.CryptoKey{}, err
1577+
}
1578+
return q.db.GetLatestCryptoKeyByFeature(ctx, feature)
1579+
}
1580+
15521581
func (q *querier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
15531582
if _, err := q.GetWorkspaceByID(ctx, workspaceID); err != nil {
15541583
return database.WorkspaceBuild{}, err
@@ -2662,6 +2691,13 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
26622691
return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertAuditLog)(ctx, arg)
26632692
}
26642693

2694+
func (q *querier) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
2695+
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceCryptoKey); err != nil {
2696+
return database.CryptoKey{}, err
2697+
}
2698+
return q.db.InsertCryptoKey(ctx, arg)
2699+
}
2700+
26652701
func (q *querier) InsertCustomRole(ctx context.Context, arg database.InsertCustomRoleParams) (database.CustomRole, error) {
26662702
// Org and site role upsert share the same query. So switch the assertion based on the org uuid.
26672703
if arg.OrganizationID.UUID != uuid.Nil {
@@ -3169,6 +3205,13 @@ func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKe
31693205
return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg)
31703206
}
31713207

3208+
func (q *querier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) {
3209+
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceCryptoKey); err != nil {
3210+
return database.CryptoKey{}, err
3211+
}
3212+
return q.db.UpdateCryptoKeyDeletesAt(ctx, arg)
3213+
}
3214+
31723215
func (q *querier) UpdateCustomRole(ctx context.Context, arg database.UpdateCustomRoleParams) (database.CustomRole, error) {
31733216
if arg.OrganizationID.UUID != uuid.Nil {
31743217
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAssignOrgRole.InOrg(arg.OrganizationID.UUID)); err != nil {

coderd/database/dbauthz/dbauthz_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,6 +2253,57 @@ func (s *MethodTestSuite) TestDBCrypt() {
22532253
}))
22542254
}
22552255

2256+
func (s *MethodTestSuite) TestCryptoKeys() {
2257+
s.Run("GetCryptoKeys", s.Subtest(func(db database.Store, check *expects) {
2258+
check.Args().
2259+
Asserts(rbac.ResourceCryptoKey, policy.ActionRead)
2260+
}))
2261+
s.Run("InsertCryptoKey", s.Subtest(func(db database.Store, check *expects) {
2262+
check.Args(database.InsertCryptoKeyParams{
2263+
Feature: database.CryptoKeyFeatureWorkspaceApps,
2264+
}).
2265+
Asserts(rbac.ResourceCryptoKey, policy.ActionCreate)
2266+
}))
2267+
s.Run("DeleteCryptoKey", s.Subtest(func(db database.Store, check *expects) {
2268+
key := dbgen.CryptoKey(s.T(), db, database.CryptoKey{
2269+
Feature: database.CryptoKeyFeatureWorkspaceApps,
2270+
Sequence: 4,
2271+
})
2272+
check.Args(database.DeleteCryptoKeyParams{
2273+
Feature: key.Feature,
2274+
Sequence: key.Sequence,
2275+
}).Asserts(rbac.ResourceCryptoKey, policy.ActionDelete)
2276+
}))
2277+
s.Run("GetCryptoKeyByFeatureAndSequence", s.Subtest(func(db database.Store, check *expects) {
2278+
key := dbgen.CryptoKey(s.T(), db, database.CryptoKey{
2279+
Feature: database.CryptoKeyFeatureWorkspaceApps,
2280+
Sequence: 4,
2281+
})
2282+
check.Args(database.GetCryptoKeyByFeatureAndSequenceParams{
2283+
Feature: key.Feature,
2284+
Sequence: key.Sequence,
2285+
}).Asserts(rbac.ResourceCryptoKey, policy.ActionRead).Returns(key)
2286+
}))
2287+
s.Run("GetLatestCryptoKeyByFeature", s.Subtest(func(db database.Store, check *expects) {
2288+
dbgen.CryptoKey(s.T(), db, database.CryptoKey{
2289+
Feature: database.CryptoKeyFeatureWorkspaceApps,
2290+
Sequence: 4,
2291+
})
2292+
check.Args(database.CryptoKeyFeatureWorkspaceApps).Asserts(rbac.ResourceCryptoKey, policy.ActionRead)
2293+
}))
2294+
s.Run("UpdateCryptoKeyDeletesAt", s.Subtest(func(db database.Store, check *expects) {
2295+
key := dbgen.CryptoKey(s.T(), db, database.CryptoKey{
2296+
Feature: database.CryptoKeyFeatureWorkspaceApps,
2297+
Sequence: 4,
2298+
})
2299+
check.Args(database.UpdateCryptoKeyDeletesAtParams{
2300+
Feature: key.Feature,
2301+
Sequence: key.Sequence,
2302+
DeletesAt: sql.NullTime{Time: time.Now(), Valid: true},
2303+
}).Asserts(rbac.ResourceCryptoKey, policy.ActionUpdate)
2304+
}))
2305+
}
2306+
22562307
func (s *MethodTestSuite) TestSystemFunctions() {
22572308
s.Run("UpdateUserLinkedID", s.Subtest(func(db database.Store, check *expects) {
22582309
u := dbgen.User(s.T(), db, database.User{})

coderd/database/dbgen/dbgen.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dbgen
22

33
import (
44
"context"
5+
"crypto/rand"
56
"crypto/sha256"
67
"database/sql"
78
"encoding/hex"
@@ -16,6 +17,7 @@ import (
1617
"github.com/google/uuid"
1718
"github.com/sqlc-dev/pqtype"
1819
"github.com/stretchr/testify/require"
20+
"golang.org/x/xerrors"
1921

2022
"github.com/coder/coder/v2/coderd/database"
2123
"github.com/coder/coder/v2/coderd/database/dbauthz"
@@ -893,6 +895,40 @@ func CustomRole(t testing.TB, db database.Store, seed database.CustomRole) datab
893895
return role
894896
}
895897

898+
func CryptoKey(t testing.TB, db database.Store, seed database.CryptoKey) database.CryptoKey {
899+
t.Helper()
900+
901+
seed.Feature = takeFirst(seed.Feature, database.CryptoKeyFeatureWorkspaceApps)
902+
903+
if !seed.Secret.Valid {
904+
secret, err := newCryptoKeySecret(seed.Feature)
905+
require.NoError(t, err, "generate secret")
906+
seed.Secret = sql.NullString{
907+
String: secret,
908+
Valid: true,
909+
}
910+
}
911+
912+
key, err := db.InsertCryptoKey(genCtx, database.InsertCryptoKeyParams{
913+
Sequence: takeFirst(seed.Sequence, 123),
914+
Secret: seed.Secret,
915+
SecretKeyID: takeFirst(seed.SecretKeyID, sql.NullString{}),
916+
Feature: seed.Feature,
917+
StartsAt: takeFirst(seed.StartsAt, time.Now()),
918+
})
919+
require.NoError(t, err, "insert crypto key")
920+
921+
if seed.DeletesAt.Valid {
922+
key, err = db.UpdateCryptoKeyDeletesAt(genCtx, database.UpdateCryptoKeyDeletesAtParams{
923+
Feature: key.Feature,
924+
Sequence: key.Sequence,
925+
DeletesAt: sql.NullTime{Time: seed.DeletesAt.Time, Valid: true},
926+
})
927+
require.NoError(t, err, "update crypto key deletes_at")
928+
}
929+
return key
930+
}
931+
896932
func ProvisionerJobTimings(t testing.TB, db database.Store, seed database.InsertProvisionerJobTimingsParams) []database.ProvisionerJobTiming {
897933
timings, err := db.InsertProvisionerJobTimings(genCtx, seed)
898934
require.NoError(t, err, "insert provisioner job timings")
@@ -942,3 +978,24 @@ func takeFirst[Value comparable](values ...Value) Value {
942978
return v != empty
943979
})
944980
}
981+
982+
func newCryptoKeySecret(feature database.CryptoKeyFeature) (string, error) {
983+
switch feature {
984+
case database.CryptoKeyFeatureWorkspaceApps:
985+
return generateCryptoKey(96)
986+
case database.CryptoKeyFeatureOidcConvert:
987+
return generateCryptoKey(32)
988+
case database.CryptoKeyFeatureTailnetResume:
989+
return generateCryptoKey(64)
990+
}
991+
return "", xerrors.Errorf("unknown feature: %s", feature)
992+
}
993+
994+
func generateCryptoKey(length int) (string, error) {
995+
b := make([]byte, length)
996+
_, err := rand.Read(b)
997+
if err != nil {
998+
return "", xerrors.Errorf("rand read: %w", err)
999+
}
1000+
return hex.EncodeToString(b), nil
1001+
}

0 commit comments

Comments
 (0)