feat(rbac): add AsChatd subject to replace AsSystemRestricted in chatd (#22487)

Add a new SubjectTypeChatd RBAC subject with minimal permissions:
- Chat: CRUD
- Workspace: Read
- DeploymentConfig: Read

Replace all 10 AsSystemRestricted calls in coderd/chatd/chatd.go:
- Line 890: Use AsChatd instead of AsSystemRestricted for the background
processor context.
- Subscribe() path (5 calls): Remove system escalation entirely; these
run under the authenticated user's context from the HTTP handler.
- processChat path (4 calls): Remove redundant per-call wraps; the
context already carries AsChatd from the processor start.

Add TestAsChatd verifying allowed and denied actions.

Created using Mux (Opus 4.6)
This commit is contained in:
Cian Johnston
2026-03-02 15:57:04 +00:00
committed by GitHub
parent 8cfb294291
commit a62f2fbfc4
4 changed files with 95 additions and 20 deletions
+27
View File
@@ -694,6 +694,26 @@ var (
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
subjectChatd = rbac.Subject{
Type: rbac.SubjectTypeChatd,
FriendlyName: "Chatd",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "chatd"},
DisplayName: "Chat Daemon",
Site: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceWorkspace.Type: {policy.ActionRead},
rbac.ResourceDeploymentConfig.Type: {policy.ActionRead},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
)
// AsProvisionerd returns a context with an actor that has permissions required
@@ -808,6 +828,13 @@ func AsWorkspaceBuilder(ctx context.Context) context.Context {
return As(ctx, subjectWorkspaceBuilder)
}
// AsChatd returns a context with an actor scoped to the chat
// daemon's background worker. It can manage chats and read
// workspaces and deployment config, but nothing else.
func AsChatd(ctx context.Context) context.Context {
return As(ctx, subjectChatd)
}
var AsRemoveActor = rbac.Subject{
ID: "remove-actor",
}
+56
View File
@@ -13,6 +13,7 @@ import (
"github.com/brianvoe/gofakeit/v7"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/sqlc-dev/pqtype"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
@@ -5358,3 +5359,58 @@ func TestGetWorkspaceAgentByID_FastPath(t *testing.T) {
require.Equal(t, agent, result)
})
}
func TestAsChatd(t *testing.T) {
t.Parallel()
ctx := dbauthz.AsChatd(context.Background())
actor, ok := dbauthz.ActorFromContext(ctx)
require.True(t, ok, "actor must be present")
auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
t.Run("AllowedActions", func(t *testing.T) {
t.Parallel()
// Chat CRUD.
for _, action := range []policy.Action{
policy.ActionCreate, policy.ActionRead,
policy.ActionUpdate, policy.ActionDelete,
} {
err := auth.Authorize(ctx, actor, action, rbac.ResourceChat)
require.NoError(t, err, "chat %s should be allowed", action)
}
// Workspace read.
err := auth.Authorize(ctx, actor, policy.ActionRead, rbac.ResourceWorkspace)
require.NoError(t, err, "workspace read should be allowed")
// DeploymentConfig read.
err = auth.Authorize(ctx, actor, policy.ActionRead, rbac.ResourceDeploymentConfig)
require.NoError(t, err, "deployment config read should be allowed")
})
t.Run("DeniedActions", func(t *testing.T) {
t.Parallel()
// Cannot write workspaces.
for _, action := range []policy.Action{
policy.ActionUpdate, policy.ActionDelete,
} {
err := auth.Authorize(ctx, actor, action, rbac.ResourceWorkspace)
require.Error(t, err, "workspace %s should be denied", action)
}
// Cannot access users.
err := auth.Authorize(ctx, actor, policy.ActionRead, rbac.ResourceUser)
require.Error(t, err, "user read should be denied")
// Cannot access API keys.
err = auth.Authorize(ctx, actor, policy.ActionRead, rbac.ResourceApiKey)
require.Error(t, err, "api key read should be denied")
// Cannot access provisioner daemons.
err = auth.Authorize(ctx, actor, policy.ActionRead, rbac.ResourceProvisionerDaemon)
require.Error(t, err, "provisioner daemon read should be denied")
})
}