mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: introduce ResourceAiSeat for fine-grained AI seat RBAC (#24613)
Fixes: https://github.com/coder/internal/issues/1444
This commit is contained in:
@@ -226,6 +226,7 @@ var (
|
||||
rbac.ResourceProvisionerJobs.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionCreate},
|
||||
rbac.ResourceFile.Type: {policy.ActionCreate, policy.ActionRead},
|
||||
rbac.ResourceSystem.Type: {policy.WildcardSymbol},
|
||||
rbac.ResourceAiSeat.Type: {policy.ActionCreate}, // Required for UpsertAISeatState via SeatTracker.
|
||||
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate},
|
||||
// Unsure why provisionerd needs update and read personal
|
||||
rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
|
||||
@@ -597,6 +598,7 @@ var (
|
||||
DisplayName: "Usage Publisher",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceLicense.Type: {policy.ActionRead},
|
||||
rbac.ResourceAiSeat.Type: {policy.ActionRead}, // Required for GetActiveAISeatCount.
|
||||
// The usage publisher doesn't create events, just
|
||||
// reads/processes them.
|
||||
rbac.ResourceUsageEvent.Type: {policy.ActionRead, policy.ActionUpdate},
|
||||
@@ -624,7 +626,7 @@ var (
|
||||
},
|
||||
rbac.ResourceApiKey.Type: {policy.ActionRead}, // Validate API keys.
|
||||
rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceSystem.Type: {policy.ActionCreate}, // Required for UpsertAISeatState.
|
||||
rbac.ResourceAiSeat.Type: {policy.ActionCreate}, // Required for UpsertAISeatState.
|
||||
}),
|
||||
User: []rbac.Permission{},
|
||||
ByOrgID: map[string]rbac.OrgPermissions{},
|
||||
@@ -2470,7 +2472,7 @@ func (q *querier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Tim
|
||||
}
|
||||
|
||||
func (q *querier) GetActiveAISeatCount(ctx context.Context) (int64, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceLicense); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAiSeat); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return q.db.GetActiveAISeatCount(ctx)
|
||||
@@ -4228,7 +4230,7 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License,
|
||||
}
|
||||
|
||||
func (q *querier) GetUserAISeatStates(ctx context.Context, userIDs []uuid.UUID) ([]uuid.UUID, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAiSeat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetUserAISeatStates(ctx, userIDs)
|
||||
@@ -7376,7 +7378,7 @@ func (q *querier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg datab
|
||||
}
|
||||
|
||||
func (q *querier) UpsertAISeatState(ctx context.Context, arg database.UpsertAISeatStateParams) (bool, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceSystem); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAiSeat); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return q.db.UpsertAISeatState(ctx, arg)
|
||||
|
||||
@@ -1848,15 +1848,18 @@ func (s *MethodTestSuite) TestProvisionerJob() {
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestLicense() {
|
||||
func (s *MethodTestSuite) TestAISeat() {
|
||||
s.Run("GetActiveAISeatCount", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetActiveAISeatCount(gomock.Any()).Return(int64(100), nil).AnyTimes()
|
||||
check.Args().Asserts(rbac.ResourceLicense, policy.ActionRead).Returns(int64(100))
|
||||
check.Args().Asserts(rbac.ResourceAiSeat, policy.ActionRead).Returns(int64(100))
|
||||
}))
|
||||
s.Run("UpsertAISeatState", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().UpsertAISeatState(gomock.Any(), gomock.Any()).Return(true, nil).AnyTimes()
|
||||
check.Args(database.UpsertAISeatStateParams{}).Asserts(rbac.ResourceSystem, policy.ActionCreate)
|
||||
check.Args(database.UpsertAISeatStateParams{}).Asserts(rbac.ResourceAiSeat, policy.ActionCreate)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestLicense() {
|
||||
s.Run("GetLicenses", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
a := database.License{ID: 1}
|
||||
b := database.License{ID: 2}
|
||||
@@ -2544,7 +2547,7 @@ func (s *MethodTestSuite) TestUser() {
|
||||
ids := []uuid.UUID{a.ID, b.ID}
|
||||
seatStates := []uuid.UUID{a.ID}
|
||||
dbm.EXPECT().GetUserAISeatStates(gomock.Any(), ids).Return(seatStates, nil).AnyTimes()
|
||||
check.Args(ids).Asserts(rbac.ResourceUser, policy.ActionRead).Returns(seatStates)
|
||||
check.Args(ids).Asserts(rbac.ResourceAiSeat, policy.ActionRead).Returns(seatStates)
|
||||
}))
|
||||
s.Run("GetUserByEmailOrUsername", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
|
||||
Generated
+4
-1
@@ -220,7 +220,10 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'chat:read',
|
||||
'chat:update',
|
||||
'chat:delete',
|
||||
'chat:*'
|
||||
'chat:*',
|
||||
'ai_seat:*',
|
||||
'ai_seat:create',
|
||||
'ai_seat:read'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- These enum values cannot be removed from PostgreSQL.
|
||||
-- This migration is a no-op placeholder for rollback safety.
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_seat:*';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_seat:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_seat:read';
|
||||
@@ -224,6 +224,9 @@ const (
|
||||
ApiKeyScopeChatUpdate APIKeyScope = "chat:update"
|
||||
ApiKeyScopeChatDelete APIKeyScope = "chat:delete"
|
||||
ApiKeyScopeChat APIKeyScope = "chat:*"
|
||||
ApiKeyScopeAiSeat APIKeyScope = "ai_seat:*"
|
||||
ApiKeyScopeAiSeatCreate APIKeyScope = "ai_seat:create"
|
||||
ApiKeyScopeAiSeatRead APIKeyScope = "ai_seat:read"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -467,7 +470,10 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeChatRead,
|
||||
ApiKeyScopeChatUpdate,
|
||||
ApiKeyScopeChatDelete,
|
||||
ApiKeyScopeChat:
|
||||
ApiKeyScopeChat,
|
||||
ApiKeyScopeAiSeat,
|
||||
ApiKeyScopeAiSeatCreate,
|
||||
ApiKeyScopeAiSeatRead:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -680,6 +686,9 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeChatUpdate,
|
||||
ApiKeyScopeChatDelete,
|
||||
ApiKeyScopeChat,
|
||||
ApiKeyScopeAiSeat,
|
||||
ApiKeyScopeAiSeatCreate,
|
||||
ApiKeyScopeAiSeatRead,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user