mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add boundary_log rbac resource (#24810)
RFC: [Bridge ↔ Boundaries Correlation RFC](https://www.notion.so/coderhq/Gateway-and-Firewall-Correlation-RFC-31ad579be592803aa8b3d48348ccdde9) Register a dedicated `boundary_log` RBAC resource type with `create`, `read`, and `delete` actions, replacing the placeholder `rbac.ResourceAuditLog` and `rbac.ResourceSystem` references previously used in the dbauthz layer. Create is granted at user-level so workspace agents can only write logs owned by their workspace owner, preventing cross-workspace log fabrication. Delete is restricted to `DBPurge` only; no human role (including owner) can delete boundary logs. | Subject | Create (own) | Create (other) | Read (all) | Delete | |---|---|---|---|---| | Workspace agent | yes | no | no | no | | Owner (site admin) | yes (via member) | no | yes | no | | Auditor | no | no | yes | no | | DBPurge | no | no | no | yes | ### Changes - **RBAC policy & resource definition**: add `boundary_log` to `policy.go` and generate `ResourceBoundaryLog` object, scope constants, and codersdk/TypeScript types. - **dbauthz authorization**: replace all `ResourceAuditLog`/`ResourceSystem` placeholders with `ResourceBoundaryLog`. `InsertBoundaryLog` and `InsertBoundarySession` derive the workspace owner from the agent and authorize with `.WithOwner()` for user-scoped create. - **Role assignments:** - **Owner (site):** read only. Excluded from `allPermsExcept` wildcard; create is inherited from member at user-level. - **Member (user-level):** create. User-scoped so agents can only write logs they own. - **Auditor (site):** read. - `boundary_log` is excluded from org-admin, org-member, and org-service-account `allPermsExcept` calls for consistency with `ResourceBoundaryUsage`. - **System subjects:** - **DB Purge** (`SubjectTypeDBPurge`): delete. The only subject that can remove boundary logs. - **Workspace agent scope**: `ResourceBoundaryLog` with wildcard ID in the agent scope allow-list (necessary for creation since no pre-existing ID exists). User-level role scoping prevents deployment-wide access. - **DB migration** (`000510_boundary_log_scopes`): add `boundary_log:*`, `boundary_log:create`, `boundary_log:delete`, `boundary_log:read` enum values to `api_key_scope`. - **Test coverage**: `BoundaryLogCreate` (user-scoped, only matching owner succeeds), `BoundaryLogDelete` (all human roles denied), `BoundaryLogRead` (owner + auditor). dbauthz mock tests set up workspace agent lookups for owner derivation. - **Generated docs**: update OpenAPI specs, API reference docs, and frontend type definitions. --------- Co-authored-by: Muhammad Danish <mdanishkhdev@gmail.com> Co-authored-by: Coder Agents <coder-agents-review[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Muhammad Danish
Coder Agents
parent
88060b846e
commit
a586b7e5e0
@@ -651,6 +651,8 @@ var (
|
||||
rbac.ResourceAibridgeInterception.Type: {policy.ActionDelete},
|
||||
// Chat auto-archive sets archived=true on inactive chats.
|
||||
rbac.ResourceChat.Type: {policy.ActionRead, policy.ActionUpdate},
|
||||
// Purge old boundary logs past the retention period.
|
||||
rbac.ResourceBoundaryLog.Type: {policy.ActionDelete},
|
||||
}),
|
||||
User: []rbac.Permission{},
|
||||
ByOrgID: map[string]rbac.OrgPermissions{},
|
||||
@@ -2191,9 +2193,8 @@ func (q *querier) DeleteOldAuditLogs(ctx context.Context, arg database.DeleteOld
|
||||
return q.db.DeleteOldAuditLogs(ctx, arg)
|
||||
}
|
||||
|
||||
// TODO (PR #24810): Replace rbac.ResourceSystem with dedicated boundary_log resource type.
|
||||
func (q *querier) DeleteOldBoundaryLogs(ctx context.Context, arg database.DeleteOldBoundaryLogsParams) (int64, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceBoundaryLog); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return q.db.DeleteOldBoundaryLogs(ctx, arg)
|
||||
@@ -2780,17 +2781,15 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI
|
||||
return q.db.GetAuthorizationUserRoles(ctx, userID)
|
||||
}
|
||||
|
||||
// TODO (PR #24810): Replace rbac.ResourceAuditLog with dedicated boundary_log resource type.
|
||||
func (q *querier) GetBoundaryLogByID(ctx context.Context, id uuid.UUID) (database.BoundaryLog, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceBoundaryLog); err != nil {
|
||||
return database.BoundaryLog{}, err
|
||||
}
|
||||
return q.db.GetBoundaryLogByID(ctx, id)
|
||||
}
|
||||
|
||||
// TODO (PR #24810): Replace rbac.ResourceAuditLog with dedicated boundary_log resource type.
|
||||
func (q *querier) GetBoundarySessionByID(ctx context.Context, id uuid.UUID) (database.BoundarySession, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceBoundaryLog); err != nil {
|
||||
return database.BoundarySession{}, err
|
||||
}
|
||||
return q.db.GetBoundarySessionByID(ctx, id)
|
||||
@@ -5468,14 +5467,29 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
|
||||
return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertAuditLog)(ctx, arg)
|
||||
}
|
||||
|
||||
// TODO (PR #24810): Replace rbac.ResourceAuditLog with dedicated boundary_log resource type.
|
||||
func (q *querier) InsertBoundaryLog(ctx context.Context, arg database.InsertBoundaryLogParams) (database.BoundaryLog, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertBoundaryLog)(ctx, arg)
|
||||
func (q *querier) InsertBoundaryLogs(ctx context.Context, arg database.InsertBoundaryLogsParams) ([]database.BoundaryLog, error) {
|
||||
session, err := q.db.GetBoundarySessionByID(ctx, arg.SessionID)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("get boundary session for owner: %w", err)
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate,
|
||||
rbac.ResourceBoundaryLog.WithOwner(session.OwnerID.UUID.String())); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.InsertBoundaryLogs(ctx, arg)
|
||||
}
|
||||
|
||||
// TODO (PR #24810): Replace rbac.ResourceAuditLog with dedicated boundary_log resource type.
|
||||
func (q *querier) InsertBoundarySession(ctx context.Context, arg database.InsertBoundarySessionParams) (database.BoundarySession, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceAuditLog, q.db.InsertBoundarySession)(ctx, arg)
|
||||
row, err := q.db.GetWorkspaceAgentAndWorkspaceByID(ctx, arg.WorkspaceAgentID)
|
||||
if err != nil {
|
||||
return database.BoundarySession{}, xerrors.Errorf("get workspace for boundary session owner: %w", err)
|
||||
}
|
||||
arg.OwnerID = uuid.NullUUID{UUID: row.WorkspaceTable.OwnerID, Valid: true}
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate,
|
||||
rbac.ResourceBoundaryLog.WithOwner(arg.OwnerID.UUID.String())); err != nil {
|
||||
return database.BoundarySession{}, err
|
||||
}
|
||||
return q.db.InsertBoundarySession(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams) (database.Chat, error) {
|
||||
@@ -6191,9 +6205,8 @@ func (q *querier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context,
|
||||
return q.db.ListAIBridgeUserPromptsByInterceptionIDs(ctx, interceptionIDs)
|
||||
}
|
||||
|
||||
// TODO (PR #24810): Replace rbac.ResourceAuditLog with dedicated boundary_log resource type.
|
||||
func (q *querier) ListBoundaryLogsBySessionID(ctx context.Context, arg database.ListBoundaryLogsBySessionIDParams) ([]database.BoundaryLog, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAuditLog); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceBoundaryLog); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.ListBoundaryLogsBySessionID(ctx, arg)
|
||||
|
||||
@@ -440,35 +440,55 @@ func (s *MethodTestSuite) TestAuditLogs() {
|
||||
}))
|
||||
}
|
||||
|
||||
// TODO (PR #24810): These RBAC assertions use placeholder resource types.
|
||||
// They will be updated when the dedicated boundary_log resource type is added.
|
||||
func (s *MethodTestSuite) TestBoundaryLogs() {
|
||||
s.Run("InsertBoundarySession", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.InsertBoundarySessionParams{}
|
||||
dbm.EXPECT().InsertBoundarySession(gomock.Any(), arg).Return(database.BoundarySession{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAuditLog, policy.ActionCreate)
|
||||
s.Run("InsertBoundarySession", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
aww := testutil.Fake(s.T(), faker, database.GetWorkspaceAgentAndWorkspaceByIDRow{})
|
||||
arg := database.InsertBoundarySessionParams{
|
||||
WorkspaceAgentID: aww.WorkspaceAgent.ID,
|
||||
}
|
||||
dbm.EXPECT().GetWorkspaceAgentAndWorkspaceByID(gomock.Any(), aww.WorkspaceAgent.ID).Return(aww, nil).AnyTimes()
|
||||
expectedArg := database.InsertBoundarySessionParams{
|
||||
WorkspaceAgentID: aww.WorkspaceAgent.ID,
|
||||
OwnerID: uuid.NullUUID{UUID: aww.WorkspaceTable.OwnerID, Valid: true},
|
||||
}
|
||||
dbm.EXPECT().InsertBoundarySession(gomock.Any(), expectedArg).Return(database.BoundarySession{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(
|
||||
rbac.ResourceBoundaryLog.WithOwner(aww.WorkspaceTable.OwnerID.String()), policy.ActionCreate,
|
||||
)
|
||||
}))
|
||||
s.Run("GetBoundarySessionByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetBoundarySessionByID(gomock.Any(), uuid.Nil).Return(database.BoundarySession{}, nil).AnyTimes()
|
||||
check.Args(uuid.Nil).Asserts(rbac.ResourceAuditLog, policy.ActionRead)
|
||||
check.Args(uuid.Nil).Asserts(rbac.ResourceBoundaryLog, policy.ActionRead)
|
||||
}))
|
||||
s.Run("InsertBoundaryLog", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.InsertBoundaryLogParams{}
|
||||
dbm.EXPECT().InsertBoundaryLog(gomock.Any(), arg).Return(database.BoundaryLog{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAuditLog, policy.ActionCreate)
|
||||
s.Run("InsertBoundaryLogs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
ownerID := uuid.New()
|
||||
sessionID := uuid.New()
|
||||
session := database.BoundarySession{
|
||||
ID: sessionID,
|
||||
OwnerID: uuid.NullUUID{UUID: ownerID, Valid: true},
|
||||
}
|
||||
arg := database.InsertBoundaryLogsParams{
|
||||
SessionID: sessionID,
|
||||
ID: []uuid.UUID{uuid.New(), uuid.New()},
|
||||
}
|
||||
dbm.EXPECT().GetBoundarySessionByID(gomock.Any(), sessionID).Return(session, nil).AnyTimes()
|
||||
dbm.EXPECT().InsertBoundaryLogs(gomock.Any(), arg).Return([]database.BoundaryLog{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(
|
||||
rbac.ResourceBoundaryLog.WithOwner(ownerID.String()), policy.ActionCreate,
|
||||
)
|
||||
}))
|
||||
s.Run("GetBoundaryLogByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetBoundaryLogByID(gomock.Any(), uuid.Nil).Return(database.BoundaryLog{}, nil).AnyTimes()
|
||||
check.Args(uuid.Nil).Asserts(rbac.ResourceAuditLog, policy.ActionRead)
|
||||
check.Args(uuid.Nil).Asserts(rbac.ResourceBoundaryLog, policy.ActionRead)
|
||||
}))
|
||||
s.Run("ListBoundaryLogsBySessionID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.ListBoundaryLogsBySessionIDParams{}
|
||||
dbm.EXPECT().ListBoundaryLogsBySessionID(gomock.Any(), arg).Return([]database.BoundaryLog{}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceAuditLog, policy.ActionRead)
|
||||
check.Args(arg).Asserts(rbac.ResourceBoundaryLog, policy.ActionRead)
|
||||
}))
|
||||
s.Run("DeleteOldBoundaryLogs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().DeleteOldBoundaryLogs(gomock.Any(), database.DeleteOldBoundaryLogsParams{}).Return(int64(0), nil).AnyTimes()
|
||||
check.Args(database.DeleteOldBoundaryLogsParams{}).Asserts(rbac.ResourceSystem, policy.ActionDelete)
|
||||
check.Args(database.DeleteOldBoundaryLogsParams{}).Asserts(rbac.ResourceBoundaryLog, policy.ActionDelete)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user