Skip to content
Next Next commit
feat: add schema for key rotation
  • Loading branch information
sreya committed Sep 13, 2024
commit 03262de99de5792f8fbdf7460467cad31aa18dc3
42 changes: 42 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,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.ResourceSystem); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a new object type for CryptoKeys

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and did this but I'm not sure what adding an object type buys us? I don't believe there's any intent to expose this to users so distinguishing it from other "system resources" doesn't seem to buy us anything.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple things:

  1. Clarity and understandability. "System resources" is currently a dumping ground of a bunch of misc stuff that doesn't actually make sense together. Let's not make it worse. Crypto keys are a specific distinct resource and our code should reflect that.
  2. Limited permissions scope. You're about to write a new subcomponent that interacts with crypto keys. What permissions should it have? Answer: crypto keys and nothing else. Impossible to do if you lump crypto keys in with a bunch of other stuff.

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 {
Expand Down Expand Up @@ -1383,6 +1390,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.ResourceSystem); 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.ResourceSystem); 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
Expand Down Expand Up @@ -1549,6 +1570,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.ResourceSystem); 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
Expand Down Expand Up @@ -2654,6 +2682,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.ResourceSystem); 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 {
Expand Down Expand Up @@ -3157,6 +3192,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.ResourceSystem); 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 {
Expand Down
31 changes: 31 additions & 0 deletions coderd/database/dbgen/dbgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dbgen

import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
Expand Down Expand Up @@ -893,6 +894,36 @@ 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()

b := make([]byte, 96)
_, err := rand.Read(b)
require.NoError(t, err, "generate secret")

key, err := db.InsertCryptoKey(genCtx, database.InsertCryptoKeyParams{
Sequence: takeFirst(seed.Sequence, 123),
Secret: takeFirst(seed.Secret, sql.NullString{
String: hex.EncodeToString(b),
Valid: true,
}),
SecretKeyID: takeFirst(seed.SecretKeyID, sql.NullString{}),
Feature: takeFirst(seed.Feature, database.CryptoKeyFeatureWorkspaceApps),
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 must[V any](v V, err error) V {
if err != nil {
panic(err)
Expand Down
113 changes: 113 additions & 0 deletions coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ type data struct {
// New tables
workspaceAgentStats []database.WorkspaceAgentStat
auditLogs []database.AuditLog
cryptoKeys []database.CryptoKey
dbcryptKeys []database.DBCryptKey
files []database.File
externalAuthLinks []database.ExternalAuthLink
Expand Down Expand Up @@ -1434,6 +1435,25 @@ 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
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 {
Expand Down Expand Up @@ -2309,6 +2329,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()
Expand Down Expand Up @@ -2806,6 +2861,23 @@ 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.Secret.Valid {
return latestKey, nil
}
return database.CryptoKey{}, sql.ErrNoRows
}

func (q *FakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) (database.WorkspaceBuild, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
Expand Down Expand Up @@ -6305,6 +6377,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 {
Expand Down Expand Up @@ -7774,6 +7868,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 {
Expand Down
42 changes: 42 additions & 0 deletions coderd/database/dbmetrics/dbmetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading