feat: add rbac specificity for dbpurge (#21088)

Related to
[`internal#1139`](https://github.com/coder/internal/issues/1139)

Continuation of #21074 

This implements some RBAC role specificity for `dbpurge`, ensuring that
we follow the least-privileged model for removing data from the
database. It is specified as following.

```go
Site: rbac.Permissions(map[string][]policy.Action{
	// DeleteOldWorkspaceAgentLogs
	// DeleteOldWorkspaceAgentStats
	// DeleteOldProvisionerDaemons
	// DeleteOldTelemetryLocks
	// DeleteOldAuditLogConnectionEvents
	// DeleteOldConnectionLogs
	rbac.ResourceSystem.Type: {policy.ActionDelete},
	// DeleteOldNotificationMessages
	rbac.ResourceNotificationMessage.Type: {policy.ActionDelete},
	// ExpirePrebuildsAPIKeys
	// DeleteExpiredAPIKeys
	rbac.ResourceApiKey.Type: {policy.ActionDelete},
	// DeleteOldAIBridgeRecords
	rbac.ResourceAibridgeInterception.Type: {policy.ActionDelete},
}),
```

| Position | Pull-request |
| -------- | ------------ |
| | [feat: add prometheus observability metrics for
`dbpurge`](https://github.com/coder/coder/pull/21074) |
|  | [feat: add rbac specificity for
`dbpurge`](https://github.com/coder/coder/pull/21088) |
This commit is contained in:
Jake Howell
2025-12-20 01:02:39 +11:00
committed by GitHub
parent 00793cc0b5
commit ea00e72063
5 changed files with 90 additions and 2 deletions
+27
View File
@@ -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",
}
+2 -2
View File
@@ -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",
+59
View File
@@ -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
+1
View File
@@ -50,6 +50,7 @@ var actorLogOrder = []rbac.SubjectType{
rbac.SubjectTypeAutostart,
rbac.SubjectTypeCryptoKeyReader,
rbac.SubjectTypeCryptoKeyRotator,
rbac.SubjectTypeDBPurge,
rbac.SubjectTypeJobReaper,
rbac.SubjectTypeNotifier,
rbac.SubjectTypePrebuildsOrchestrator,
+1
View File
@@ -79,6 +79,7 @@ const (
SubjectTypeFileReader SubjectType = "file_reader"
SubjectTypeUsagePublisher SubjectType = "usage_publisher"
SubjectAibridged SubjectType = "aibridged"
SubjectTypeDBPurge SubjectType = "dbpurge"
)
const (