fix: grant AsAIBridged ResourceSystem.ActionCreate for UpsertAISeatState (#24603)

Related coder/internal#1444
This commit is contained in:
Marcin Tojek
2026-04-22 16:38:57 +02:00
committed by GitHub
parent 3362b5ae7e
commit ec91ac5427
2 changed files with 38 additions and 0 deletions
+1
View File
@@ -622,6 +622,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.
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
+37
View File
@@ -5,14 +5,19 @@ import (
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"cdr.dev/slog/v3/sloggers/slogtest"
agplaiseats "github.com/coder/coder/v2/coderd/aiseats"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/coderdtest"
"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/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/rbac"
enterpriseaiseats "github.com/coder/coder/v2/enterprise/aiseats"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
@@ -37,6 +42,38 @@ func TestSeatTrackerDB(t *testing.T) {
require.EqualValues(t, 1, count)
})
// Regression test for coder/internal#1444: UpsertAISeatState must
// succeed when called through the AsAIBridged RBAC subject. The
// aibridged daemon context was missing ResourceSystem.ActionCreate,
// which caused the very first RecordUsage call per user to fail
// with "unauthorized: rbac: forbidden".
t.Run("AsAIBridgedRBAC", func(t *testing.T) {
t.Parallel()
rawDB, _ := dbtestutil.NewDB(t)
authz := rbac.NewStrictAuthorizer(prometheus.NewRegistry())
authzDB := dbauthz.New(rawDB, authz, slogtest.Make(t, nil), coderdtest.AccessControlStorePointer())
ctx := testutil.Context(t, testutil.WaitShort)
clock := quartz.NewMock(t)
tracker := enterpriseaiseats.New(authzDB, testutil.Logger(t), clock, nil)
// Insert a user directly in the raw DB so it exists for the
// foreign key reference.
user := dbgen.User(t, rawDB, database.User{Status: database.UserStatusActive})
// Call RecordUsage with the AIBridged context, mirroring the
// production call path in aibridgedserver.RecordInterception.
aibridgedCtx := dbauthz.AsAIBridged(ctx)
tracker.RecordUsage(aibridgedCtx, user.ID, agplaiseats.ReasonAIBridge("provider=test, model=test"))
// Verify the seat was actually recorded. A count of 0 means
// the upsert was silently rejected by RBAC.
count, err := rawDB.GetActiveAISeatCount(ctx)
require.NoError(t, err)
require.EqualValues(t, 1, count, "AI seat should be recorded when using AsAIBridged context")
})
t.Run("InactiveUsersExcluded", func(t *testing.T) {
t.Parallel()