diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 4962949f7f..b5747fb970 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -616,6 +616,27 @@ var ( }), Scope: rbac.ScopeAll, }.WithCachedASTValue() + + subjectDBPurge = rbac.Subject{ + Type: rbac.SubjectTypeDBPurge, + FriendlyName: "DB Purge", + ID: uuid.Nil.String(), + Roles: rbac.Roles([]rbac.Role{ + { + Identifier: rbac.RoleIdentifier{Name: "dbpurge"}, + DisplayName: "DB Purge Daemon", + Site: rbac.Permissions(map[string][]policy.Action{ + rbac.ResourceSystem.Type: {policy.ActionDelete}, + rbac.ResourceNotificationMessage.Type: {policy.ActionDelete}, + rbac.ResourceApiKey.Type: {policy.ActionDelete}, + rbac.ResourceAibridgeInterception.Type: {policy.ActionDelete}, + }), + User: []rbac.Permission{}, + ByOrgID: map[string]rbac.OrgPermissions{}, + }, + }), + Scope: rbac.ScopeAll, + }.WithCachedASTValue() ) // AsProvisionerd returns a context with an actor that has permissions required @@ -710,6 +731,12 @@ func AsAIBridged(ctx context.Context) context.Context { return As(ctx, subjectAibridged) } +// AsDBPurge returns a context with an actor that has permissions required +// for dbpurge to delete old database records. +func AsDBPurge(ctx context.Context) context.Context { + return As(ctx, subjectDBPurge) +} + var AsRemoveActor = rbac.Subject{ ID: "remove-actor", } diff --git a/coderd/database/dbpurge/dbpurge.go b/coderd/database/dbpurge/dbpurge.go index 540724909f..9587d74396 100644 --- a/coderd/database/dbpurge/dbpurge.go +++ b/coderd/database/dbpurge/dbpurge.go @@ -46,8 +46,8 @@ func New(ctx context.Context, logger slog.Logger, db database.Store, vals *coder closed := make(chan struct{}) ctx, cancelFunc := context.WithCancel(ctx) - //nolint:gocritic // The system purges old db records without user input. - ctx = dbauthz.AsSystemRestricted(ctx) + //nolint:gocritic // Use dbpurge-specific subject with minimal permissions. + ctx = dbauthz.AsDBPurge(ctx) iterationDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "coderd", diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index 68bad45cda..96c79d4ae3 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -22,8 +22,10 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest/promhelp" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbgen" "github.com/coder/coder/v2/coderd/database/dbmock" "github.com/coder/coder/v2/coderd/database/dbpurge" @@ -31,6 +33,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/provisionerdserver" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionerd/proto" "github.com/coder/coder/v2/provisionersdk" @@ -1631,6 +1634,62 @@ func TestDeleteExpiredAPIKeys(t *testing.T) { } } +func TestDBPurgeAuthorization(t *testing.T) { + t.Parallel() + + t.Run("DBPurgeActorCanCallPurgeOperations", func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitShort) + rawDB, _ := dbtestutil.NewDB(t) + + authz := rbac.NewAuthorizer(prometheus.NewRegistry()) + db := dbauthz.New(rawDB, authz, testutil.Logger(t), coderdtest.AccessControlStorePointer()) + + ctx = dbauthz.AsDBPurge(ctx) + + actor, ok := dbauthz.ActorFromContext(ctx) + require.True(t, ok, "actor should be present") + require.Equal(t, rbac.SubjectTypeDBPurge, actor.Type, "should be DBPurge type") + require.Contains(t, actor.Roles.Names(), rbac.RoleIdentifier{Name: "dbpurge"}, + "should have dbpurge role") + + _, err := db.DeleteOldWorkspaceAgentLogs(ctx, time.Now().Add(-24*time.Hour)) + require.NoError(t, err) + + err = db.DeleteOldWorkspaceAgentStats(ctx) + require.NoError(t, err) + + err = db.DeleteOldProvisionerDaemons(ctx) + require.NoError(t, err) + + err = db.DeleteOldNotificationMessages(ctx) + require.NoError(t, err) + + err = db.ExpirePrebuildsAPIKeys(ctx, time.Now().Add(-24*time.Hour)) + require.NoError(t, err) + + params := database.DeleteExpiredAPIKeysParams{ + Before: time.Now().Add(-24 * time.Hour), + LimitCount: 100, + } + _, err = db.DeleteExpiredAPIKeys(ctx, params) + require.NoError(t, err) + + err = db.DeleteOldAuditLogConnectionEvents(ctx, database.DeleteOldAuditLogConnectionEventsParams{ + BeforeTime: time.Now().Add(-24 * time.Hour), + LimitCount: 100, + }) + require.NoError(t, err) + + _, err = db.DeleteOldAuditLogs(ctx, database.DeleteOldAuditLogsParams{ + BeforeTime: time.Now().Add(-24 * time.Hour), + LimitCount: 100, + }) + require.NoError(t, err) + }) +} + // ptr is a helper to create a pointer to a value. func ptr[T any](v T) *T { return &v diff --git a/coderd/httpmw/loggermw/logger_full.go b/coderd/httpmw/loggermw/logger_full.go index 8240289c50..e735bc102d 100644 --- a/coderd/httpmw/loggermw/logger_full.go +++ b/coderd/httpmw/loggermw/logger_full.go @@ -50,6 +50,7 @@ var actorLogOrder = []rbac.SubjectType{ rbac.SubjectTypeAutostart, rbac.SubjectTypeCryptoKeyReader, rbac.SubjectTypeCryptoKeyRotator, + rbac.SubjectTypeDBPurge, rbac.SubjectTypeJobReaper, rbac.SubjectTypeNotifier, rbac.SubjectTypePrebuildsOrchestrator, diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 2f39cf32a7..7a8ad7e813 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -79,6 +79,7 @@ const ( SubjectTypeFileReader SubjectType = "file_reader" SubjectTypeUsagePublisher SubjectType = "usage_publisher" SubjectAibridged SubjectType = "aibridged" + SubjectTypeDBPurge SubjectType = "dbpurge" ) const (