mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore: add aibridge database resources & define RBAC policies (#19796)
Closes https://github.com/coder/internal/issues/986
This commit is contained in:
@@ -175,6 +175,22 @@ func (q *querier) authorizePrebuiltWorkspace(ctx context.Context, action policy.
|
||||
return xerrors.Errorf("authorize context: %w", workspaceErr)
|
||||
}
|
||||
|
||||
// authorizeAIBridgeInterceptionUpdate validates that the context's actor matches the initiator of the AIBridgeInterception.
|
||||
// This is used by all of the sub-resources which fall under the [ResourceAibridgeInterception] umbrella.
|
||||
func (q *querier) authorizeAIBridgeInterceptionUpdate(ctx context.Context, interceptionID uuid.UUID) error {
|
||||
inter, err := q.db.GetAIBridgeInterceptionByID(ctx, interceptionID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch aibridge interception %q: %w", interceptionID, err)
|
||||
}
|
||||
|
||||
err = q.authorizeContext(ctx, policy.ActionUpdate, inter.RBACObject())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type authContextKey struct{}
|
||||
|
||||
// ActorFromContext returns the authorization subject from the context.
|
||||
@@ -542,6 +558,29 @@ var (
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
|
||||
// See aibridged package.
|
||||
subjectAibridged = rbac.Subject{
|
||||
Type: rbac.SubjectAibridged,
|
||||
FriendlyName: "AIBridge Daemon",
|
||||
ID: uuid.Nil.String(),
|
||||
Roles: rbac.Roles([]rbac.Role{
|
||||
{
|
||||
Identifier: rbac.RoleIdentifier{Name: "aibridged"},
|
||||
DisplayName: "AIBridge Daemon",
|
||||
Site: rbac.Permissions(map[string][]policy.Action{
|
||||
rbac.ResourceUser.Type: {
|
||||
policy.ActionReadPersonal, // Required to read users' external auth links. // TODO: this is too broad; reduce scope to just external_auth_links by creating separate resource.
|
||||
},
|
||||
rbac.ResourceApiKey.Type: {policy.ActionRead}, // Validate API keys.
|
||||
rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
|
||||
}),
|
||||
Org: map[string][]rbac.Permission{},
|
||||
User: []rbac.Permission{},
|
||||
},
|
||||
}),
|
||||
Scope: rbac.ScopeAll,
|
||||
}.WithCachedASTValue()
|
||||
)
|
||||
|
||||
// AsProvisionerd returns a context with an actor that has permissions required
|
||||
@@ -624,6 +663,12 @@ func AsUsagePublisher(ctx context.Context) context.Context {
|
||||
return As(ctx, subjectUsagePublisher)
|
||||
}
|
||||
|
||||
// AsAIBridged returns a context with an actor that has permissions
|
||||
// required for creating, reading, and updating aibridge-related resources.
|
||||
func AsAIBridged(ctx context.Context) context.Context {
|
||||
return As(ctx, subjectAibridged)
|
||||
}
|
||||
|
||||
var AsRemoveActor = rbac.Subject{
|
||||
ID: "remove-actor",
|
||||
}
|
||||
@@ -1878,6 +1923,10 @@ func (q *querier) FindMatchingPresetID(ctx context.Context, arg database.FindMat
|
||||
return q.db.FindMatchingPresetID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (database.AIBridgeInterception, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAIBridgeInterceptionByID)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
|
||||
}
|
||||
@@ -3757,6 +3806,34 @@ func (q *querier) GetWorkspacesEligibleForTransition(ctx context.Context, now ti
|
||||
return q.db.GetWorkspacesEligibleForTransition(ctx, now)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIBridgeInterception(ctx context.Context, arg database.InsertAIBridgeInterceptionParams) (database.AIBridgeInterception, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceAibridgeInterception.WithOwner(arg.InitiatorID.String()), q.db.InsertAIBridgeInterception)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIBridgeTokenUsage(ctx context.Context, arg database.InsertAIBridgeTokenUsageParams) error {
|
||||
// All aibridge_token_usages records belong to the initiator of their associated interception.
|
||||
if err := q.authorizeAIBridgeInterceptionUpdate(ctx, arg.InterceptionID); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.InsertAIBridgeTokenUsage(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIBridgeToolUsage(ctx context.Context, arg database.InsertAIBridgeToolUsageParams) error {
|
||||
// All aibridge_tool_usages records belong to the initiator of their associated interception.
|
||||
if err := q.authorizeAIBridgeInterceptionUpdate(ctx, arg.InterceptionID); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.InsertAIBridgeToolUsage(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAIBridgeUserPrompt(ctx context.Context, arg database.InsertAIBridgeUserPromptParams) error {
|
||||
// All aibridge_user_prompts records belong to the initiator of their associated interception.
|
||||
if err := q.authorizeAIBridgeInterceptionUpdate(ctx, arg.InterceptionID); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.InsertAIBridgeUserPrompt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertAPIKey(ctx context.Context, arg database.InsertAPIKeyParams) (database.APIKey, error) {
|
||||
// TODO(Cian): ideally this would be encoded in the policy, but system users are just members and we
|
||||
// don't currently have a capability to conditionally deny creating resources by owner ID in a role.
|
||||
|
||||
@@ -4332,3 +4332,55 @@ func TestInsertAPIKey_AsPrebuildsUser(t *testing.T) {
|
||||
_, err := dbz.InsertAPIKey(ctx, testutil.Fake(t, faker, database.InsertAPIKeyParams{}))
|
||||
require.True(t, dbauthz.IsNotAuthorizedError(err))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestAIBridge() {
|
||||
s.Run("GetAIBridgeInterceptionByID", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
sessID := uuid.UUID{2}
|
||||
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
|
||||
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes()
|
||||
check.Args(sessID).Asserts(sess, policy.ActionRead).Returns(sess)
|
||||
}))
|
||||
|
||||
s.Run("InsertAIBridgeInterception", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
initID := uuid.UUID{3}
|
||||
user := testutil.Fake(s.T(), faker, database.User{ID: initID})
|
||||
// testutil.Fake cannot distinguish between a zero value and an explicitly requested value which is equivalent.
|
||||
user.IsSystem = false
|
||||
user.Deleted = false
|
||||
|
||||
sessID := uuid.UUID{2}
|
||||
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID, InitiatorID: initID})
|
||||
|
||||
params := database.InsertAIBridgeInterceptionParams{ID: sess.ID, InitiatorID: sess.InitiatorID, Provider: sess.Provider, Model: sess.Model}
|
||||
db.EXPECT().GetUserByID(gomock.Any(), initID).Return(user, nil).AnyTimes() // Validation.
|
||||
db.EXPECT().InsertAIBridgeInterception(gomock.Any(), params).Return(sess, nil).AnyTimes()
|
||||
check.Args(params).Asserts(sess, policy.ActionCreate)
|
||||
}))
|
||||
|
||||
s.Run("InsertAIBridgeTokenUsage", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
sessID := uuid.UUID{2}
|
||||
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
|
||||
params := database.InsertAIBridgeTokenUsageParams{InterceptionID: sess.ID}
|
||||
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes() // Validation.
|
||||
db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), params).Return(nil).AnyTimes()
|
||||
check.Args(params).Asserts(sess, policy.ActionUpdate)
|
||||
}))
|
||||
|
||||
s.Run("InsertAIBridgeToolUsage", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
sessID := uuid.UUID{2}
|
||||
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
|
||||
params := database.InsertAIBridgeToolUsageParams{InterceptionID: sess.ID}
|
||||
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes() // Validation.
|
||||
db.EXPECT().InsertAIBridgeToolUsage(gomock.Any(), params).Return(nil).AnyTimes()
|
||||
check.Args(params).Asserts(sess, policy.ActionUpdate)
|
||||
}))
|
||||
|
||||
s.Run("InsertAIBridgeUserPrompt", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
sessID := uuid.UUID{2}
|
||||
sess := testutil.Fake(s.T(), faker, database.AIBridgeInterception{ID: sessID})
|
||||
params := database.InsertAIBridgeUserPromptParams{InterceptionID: sess.ID}
|
||||
db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), sessID).Return(sess, nil).AnyTimes() // Validation.
|
||||
db.EXPECT().InsertAIBridgeUserPrompt(gomock.Any(), params).Return(nil).AnyTimes()
|
||||
check.Args(params).Asserts(sess, policy.ActionUpdate)
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user