chore!: remove members' ability to read their own interceptions; rationalize RBAC requirements (#23320)

_Disclaimer:_ _produced_ _by_ _Claude_ _Opus_ _4\.6,_ _reviewed_ _by_ _me._

**This is a breaking change.** Users who are not have `owner` or sitewide `auditor` roles will no longer be able to view interceptions.  
Regular users should not need to view this information; in fact, it could be used by a malicious insider to see what information we track and don't track to exfiltrate data or perform actions unobserved.

---

Changed authorization for AI Bridge interception-related operations from system-level permissions to resource-specific permissions. The following functions now authorize against `rbac.ResourceAibridgeInterception` instead of `rbac.ResourceSystem`:

- `ListAIBridgeTokenUsagesByInterceptionIDs`
- `ListAIBridgeToolUsagesByInterceptionIDs`
- `ListAIBridgeUserPromptsByInterceptionIDs`

Updated RBAC roles to grant AI Bridge interception permissions:

- **User/Member roles**: Can create and update AI Bridge interceptions but cannot read them back
- **Service accounts**: Same create/update permissions without read access
- **Owners/Auditors**: Retain full read access to all interceptions

Removed system-level authorization bypass in `populatedAndConvertAIBridgeInterceptions` function, allowing proper resource-level authorization checks.

Updated tests to reflect the new permission model where members cannot view AI Bridge interceptions, even their own, while owners and auditors maintain full visibility.
This commit is contained in:
Danny Kopping
2026-03-24 12:03:20 +02:00
committed by GitHub
parent 245ce91199
commit dba9f68b11
7 changed files with 191 additions and 63 deletions
+3 -9
View File
@@ -5334,9 +5334,7 @@ func (q *querier) ListAIBridgeSessions(ctx context.Context, arg database.ListAIB
}
func (q *querier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeTokenUsage, error) {
// This function is a system function until we implement a join for aibridge interceptions.
// Matches the behavior of the workspaces listing endpoint.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAibridgeInterception); err != nil {
return nil, err
}
@@ -5344,9 +5342,7 @@ func (q *querier) ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context,
}
func (q *querier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeToolUsage, error) {
// This function is a system function until we implement a join for aibridge interceptions.
// Matches the behavior of the workspaces listing endpoint.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAibridgeInterception); err != nil {
return nil, err
}
@@ -5354,9 +5350,7 @@ func (q *querier) ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, i
}
func (q *querier) ListAIBridgeUserPromptsByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeUserPrompt, error) {
// This function is a system function until we implement a join for aibridge interceptions.
// Matches the behavior of the workspaces listing endpoint.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err != nil {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAibridgeInterception); err != nil {
return nil, err
}
+3 -3
View File
@@ -5545,19 +5545,19 @@ func (s *MethodTestSuite) TestAIBridge() {
s.Run("ListAIBridgeTokenUsagesByInterceptionIDs", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{{1}}
db.EXPECT().ListAIBridgeTokenUsagesByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeTokenUsage{}, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.AIBridgeTokenUsage{})
check.Args(ids).Asserts(rbac.ResourceAibridgeInterception, policy.ActionRead).Returns([]database.AIBridgeTokenUsage{})
}))
s.Run("ListAIBridgeUserPromptsByInterceptionIDs", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{{1}}
db.EXPECT().ListAIBridgeUserPromptsByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeUserPrompt{}, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.AIBridgeUserPrompt{})
check.Args(ids).Asserts(rbac.ResourceAibridgeInterception, policy.ActionRead).Returns([]database.AIBridgeUserPrompt{})
}))
s.Run("ListAIBridgeToolUsagesByInterceptionIDs", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ids := []uuid.UUID{{1}}
db.EXPECT().ListAIBridgeToolUsagesByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeToolUsage{}, nil).AnyTimes()
check.Args(ids).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns([]database.AIBridgeToolUsage{})
check.Args(ids).Asserts(rbac.ResourceAibridgeInterception, policy.ActionRead).Returns([]database.AIBridgeToolUsage{})
}))
s.Run("UpdateAIBridgeInterceptionEnded", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
+19 -2
View File
@@ -316,13 +316,16 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
denyPermissions...,
),
User: append(
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage),
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception),
Permissions(map[string][]policy.Action{
// Users cannot do create/update/delete on themselves, but they
// can read their own details.
ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
// Users can create provisioner daemons scoped to themselves.
ResourceProvisionerDaemon.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
// Members can create and update AI Bridge interceptions but
// cannot read them back.
ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionUpdate},
})...,
),
ByOrgID: map[string]OrgPermissions{},
@@ -345,7 +348,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
// Allow auditors to query deployment stats and insights.
ResourceDeploymentStats.Type: {policy.ActionRead},
ResourceDeploymentConfig.Type: {policy.ActionRead},
// Allow auditors to query aibridge interceptions.
// Allow auditors to query AI Bridge interceptions.
ResourceAibridgeInterception.Type: {policy.ActionRead},
}),
User: []Permission{},
@@ -998,6 +1001,7 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
ResourcePrebuiltWorkspace,
ResourceUser,
ResourceOrganizationMember,
ResourceAibridgeInterception,
),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build,
@@ -1016,6 +1020,12 @@ func OrgMemberPermissions(org OrgSettings) OrgRolePermissions {
ResourceOrganizationMember.Type: {
policy.ActionRead,
},
// Members can create and update AI Bridge interceptions but
// cannot read them back.
ResourceAibridgeInterception.Type: {
policy.ActionCreate,
policy.ActionUpdate,
},
})...,
)
@@ -1073,6 +1083,7 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
ResourcePrebuiltWorkspace,
ResourceUser,
ResourceOrganizationMember,
ResourceAibridgeInterception,
),
Permissions(map[string][]policy.Action{
// Reduced permission set on dormant workspaces. No build,
@@ -1091,6 +1102,12 @@ func OrgServiceAccountPermissions(org OrgSettings) OrgRolePermissions {
ResourceOrganizationMember.Type: {
policy.ActionRead,
},
// Service accounts can create and update AI Bridge
// interceptions but cannot read them back.
ResourceAibridgeInterception.Type: {
policy.ActionCreate,
policy.ActionUpdate,
},
})...,
)
+19 -2
View File
@@ -1023,8 +1023,9 @@ func TestRolePermissions(t *testing.T) {
},
},
{
Name: "AIBridgeInterceptions",
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
// Members can create/update records but can't read them afterwards.
Name: "AIBridgeInterceptionsCreateUpdate",
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, memberMe},
@@ -1036,6 +1037,22 @@ func TestRolePermissions(t *testing.T) {
},
},
},
{
// Only owners and site-wide auditors can view interceptions and their sub-resources.
Name: "AIBridgeInterceptionsRead",
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]hasAuthSubjects{
true: {owner, auditor},
false: {
memberMe,
orgAdmin, otherOrgAdmin,
orgAuditor, otherOrgAuditor,
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
userAdmin, orgUserAdmin, otherOrgUserAdmin,
},
},
},
{
Name: "BoundaryUsage",
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
+66 -19
View File
@@ -28,7 +28,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = true
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
@@ -38,7 +38,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
},
},
})
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
_, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
now := dbtime.Now()
interception1 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
@@ -49,11 +49,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
InitiatorID: member.ID,
StartedAt: now,
}, &interception2EndedAt)
// Should not be returned because the user can't see it.
_ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
interception3EndedAt := now.Add(-time.Hour)
interception3 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: owner.UserID,
StartedAt: now.Add(-2 * time.Hour),
}, nil)
}, &interception3EndedAt)
args := []string{
"aibridge",
@@ -61,7 +61,8 @@ func TestAIBridgeListInterceptions(t *testing.T) {
"list",
}
inv, root := newCLI(t, args...)
clitest.SetupConfig(t, memberClient, root)
//nolint:gocritic // Owner can read all interceptions.
clitest.SetupConfig(t, ownerClient, root)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -70,8 +71,8 @@ func TestAIBridgeListInterceptions(t *testing.T) {
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
// Reverse order because the order is `started_at ASC`.
requireHasInterceptions(t, out.Bytes(), []uuid.UUID{interception2.ID, interception1.ID})
// Owner sees all interceptions. Ordered by started_at DESC.
requireHasInterceptions(t, out.Bytes(), []uuid.UUID{interception2.ID, interception1.ID, interception3.ID})
})
t.Run("Filter", func(t *testing.T) {
@@ -79,7 +80,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = true
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
@@ -89,7 +90,7 @@ func TestAIBridgeListInterceptions(t *testing.T) {
},
},
})
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
_, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
now := dbtime.Now()
@@ -143,12 +144,13 @@ func TestAIBridgeListInterceptions(t *testing.T) {
"list",
"--started-after", now.Add(-time.Hour).Format(time.RFC3339),
"--started-before", now.Add(time.Hour).Format(time.RFC3339),
"--initiator", codersdk.Me,
"--initiator", member.Username,
"--provider", goodInterception.Provider,
"--model", goodInterception.Model,
}
inv, root := newCLI(t, args...)
clitest.SetupConfig(t, memberClient, root)
//nolint:gocritic // Owner can read all interceptions.
clitest.SetupConfig(t, ownerClient, root)
ctx := testutil.Context(t, testutil.WaitLong)
@@ -160,12 +162,57 @@ func TestAIBridgeListInterceptions(t *testing.T) {
requireHasInterceptions(t, out.Bytes(), []uuid.UUID{goodInterception.ID})
})
t.Run("Pagination", func(t *testing.T) {
t.Run("FilterByMe", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = true
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
now := dbtime.Now()
// Create an interception initiated by the member.
_ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
StartedAt: now,
}, nil)
args := []string{
"aibridge",
"interceptions",
"list",
"--initiator", codersdk.Me,
}
inv, root := newCLI(t, args...)
clitest.SetupConfig(t, memberClient, root)
ctx := testutil.Context(t, testutil.WaitLong)
out := bytes.NewBuffer(nil)
inv.Stdout = out
err := inv.WithContext(ctx).Run()
require.NoError(t, err)
// Member cannot read their own interceptions.
requireHasInterceptions(t, out.Bytes(), []uuid.UUID{})
})
t.Run("Pagination", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = true
ownerClient, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
@@ -175,20 +222,19 @@ func TestAIBridgeListInterceptions(t *testing.T) {
},
},
})
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
now := dbtime.Now()
firstInterceptionEndedAt := now.Add(time.Minute)
firstInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
InitiatorID: owner.UserID,
StartedAt: now,
}, &firstInterceptionEndedAt)
returnedInterception := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
InitiatorID: owner.UserID,
StartedAt: now.Add(-time.Hour),
}, &now)
_ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
InitiatorID: owner.UserID,
StartedAt: now.Add(-2 * time.Hour),
}, nil)
@@ -200,7 +246,8 @@ func TestAIBridgeListInterceptions(t *testing.T) {
"--after-id", firstInterception.ID.String(),
}
inv, root := newCLI(t, args...)
clitest.SetupConfig(t, memberClient, root)
//nolint:gocritic // Owner can read all interceptions.
clitest.SetupConfig(t, ownerClient, root)
ctx := testutil.Context(t, testutil.WaitLong)
+7 -7
View File
@@ -15,7 +15,6 @@ import (
"github.com/coder/coder/v2/coderd"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/searchquery"
@@ -358,13 +357,16 @@ func (api *API) aiBridgeListModels(rw http.ResponseWriter, r *http.Request) {
}
func populatedAndConvertAIBridgeInterceptions(ctx context.Context, db database.Store, dbInterceptions []database.ListAIBridgeInterceptionsRow) ([]codersdk.AIBridgeInterception, error) {
if len(dbInterceptions) == 0 {
return []codersdk.AIBridgeInterception{}, nil
}
ids := make([]uuid.UUID, len(dbInterceptions))
for i, row := range dbInterceptions {
ids[i] = row.AIBridgeInterception.ID
}
//nolint:gocritic // This is a system function until we implement a join for aibridge interceptions. AI Bridge interception subresources use the same authorization call as their parent.
tokenUsagesRows, err := db.ListAIBridgeTokenUsagesByInterceptionIDs(dbauthz.AsSystemRestricted(ctx), ids)
tokenUsagesRows, err := db.ListAIBridgeTokenUsagesByInterceptionIDs(ctx, ids)
if err != nil {
return nil, xerrors.Errorf("get linked aibridge token usages from database: %w", err)
}
@@ -373,8 +375,7 @@ func populatedAndConvertAIBridgeInterceptions(ctx context.Context, db database.S
tokenUsagesMap[row.InterceptionID] = append(tokenUsagesMap[row.InterceptionID], row)
}
//nolint:gocritic // This is a system function until we implement a join for aibridge interceptions. AI Bridge interception subresources use the same authorization call as their parent.
userPromptRows, err := db.ListAIBridgeUserPromptsByInterceptionIDs(dbauthz.AsSystemRestricted(ctx), ids)
userPromptRows, err := db.ListAIBridgeUserPromptsByInterceptionIDs(ctx, ids)
if err != nil {
return nil, xerrors.Errorf("get linked aibridge user prompts from database: %w", err)
}
@@ -383,8 +384,7 @@ func populatedAndConvertAIBridgeInterceptions(ctx context.Context, db database.S
userPromptsMap[row.InterceptionID] = append(userPromptsMap[row.InterceptionID], row)
}
//nolint:gocritic // This is a system function until we implement a join for aibridge interceptions. AI Bridge interception subresources use the same authorization call as their parent.
toolUsagesRows, err := db.ListAIBridgeToolUsagesByInterceptionIDs(dbauthz.AsSystemRestricted(ctx), ids)
toolUsagesRows, err := db.ListAIBridgeToolUsagesByInterceptionIDs(ctx, ids)
if err != nil {
return nil, xerrors.Errorf("get linked aibridge tool usages from database: %w", err)
}
+74 -21
View File
@@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/coderd/database/db2sdk"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/cryptorand"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
@@ -369,20 +370,21 @@ func TestAIBridgeListInterceptions(t *testing.T) {
StartedAt: now.Add(-time.Hour),
}, &now)
// Admin can see all interceptions.
res, err := adminClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
// Members cannot read AIBridge interceptions, not even their
// own (i2 is owned by secondUser).
res, err := secondUserClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
require.NoError(t, err)
require.EqualValues(t, 0, res.Count)
require.Empty(t, res.Results)
// Owner can see all interceptions, including secondUser's,
// proving the data exists and the member was filtered out.
res, err = adminClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
require.NoError(t, err)
require.EqualValues(t, 2, res.Count)
require.Len(t, res.Results, 2)
require.Equal(t, i1.ID, res.Results[0].ID)
require.Equal(t, i2.ID, res.Results[1].ID)
// Second user can only see their own interceptions.
res, err = secondUserClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
require.NoError(t, err)
require.EqualValues(t, 1, res.Count)
require.Len(t, res.Results, 1)
require.Equal(t, i2.ID, res.Results[0].ID)
})
t.Run("Filter", func(t *testing.T) {
@@ -583,6 +585,41 @@ func TestAIBridgeListInterceptions(t *testing.T) {
}
})
t.Run("FilterByMe/MemberCannotReadOwn", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
ownerClient, db, firstUser := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
Options: &coderdtest.Options{
DeploymentValues: dv,
},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, firstUser.OrganizationID)
now := dbtime.Now()
// Create an interception initiated by the member.
_ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
StartedAt: now,
}, nil)
// Member cannot read their own interceptions, even when
// filtering by "me".
res, err := memberClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{
Initiator: codersdk.Me,
})
require.NoError(t, err)
require.EqualValues(t, 0, res.Count)
require.Empty(t, res.Results)
})
t.Run("FilterErrors", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
@@ -1005,12 +1042,36 @@ func TestAIBridgeListSessions(t *testing.T) {
require.Empty(t, res.Sessions)
})
t.Run("FilterByMe/MemberCannotReadOwn", func(t *testing.T) {
t.Parallel()
ownerClient, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))
ctx := testutil.Context(t, testutil.WaitLong)
memberClient, member := coderdtest.CreateAnotherUser(t, ownerClient, firstUser.OrganizationID)
now := dbtime.Now()
// Create an interception (session) initiated by the member.
_ = dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: member.ID,
StartedAt: now,
}, nil)
// Member cannot read their own sessions, even when
// filtering by "me".
res, err := memberClient.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{
Initiator: codersdk.Me,
})
require.NoError(t, err)
require.EqualValues(t, 0, res.Count)
require.Empty(t, res.Sessions)
})
t.Run("Authorized", func(t *testing.T) {
t.Parallel()
adminClient, db, firstUser := coderdenttest.NewWithDatabase(t, aibridgeOpts(t))
ctx := testutil.Context(t, testutil.WaitLong)
secondUserClient, secondUser := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID)
auditorClient, auditorUser := coderdtest.CreateAnotherUser(t, adminClient, firstUser.OrganizationID, rbac.RoleAuditor())
now := dbtime.Now()
i1EndedAt := now.Add(time.Minute)
@@ -1019,25 +1080,17 @@ func TestAIBridgeListSessions(t *testing.T) {
StartedAt: now,
}, &i1EndedAt)
i2 := dbgen.AIBridgeInterception(t, db, database.InsertAIBridgeInterceptionParams{
InitiatorID: secondUser.ID,
InitiatorID: auditorUser.ID,
StartedAt: now.Add(-time.Hour),
}, &now)
// Admin can see all sessions.
//nolint:gocritic // Intentionally testing admin/owner visibility.
res, err := adminClient.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{})
// Site-level auditors can see all sessions.
res, err := auditorClient.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{})
require.NoError(t, err)
require.EqualValues(t, 2, res.Count)
require.Len(t, res.Sessions, 2)
require.Equal(t, i1.ID.String(), res.Sessions[0].ID)
require.Equal(t, i2.ID.String(), res.Sessions[1].ID)
// Second user can only see their own sessions.
res, err = secondUserClient.AIBridgeListSessions(ctx, codersdk.AIBridgeListSessionsFilter{})
require.NoError(t, err)
require.EqualValues(t, 1, res.Count)
require.Len(t, res.Sessions, 1)
require.Equal(t, i2.ID.String(), res.Sessions[0].ID)
})
t.Run("SessionIDCollisionAcrossUsers", func(t *testing.T) {