Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

82 changes: 82 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,27 @@ 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, sessID uuid.UUID) error {
act, ok := ActorFromContext(ctx)
if !ok {
return ErrNoActor
}

sess, err := q.db.GetAIBridgeInterceptionByID(ctx, sessID)
if err != nil {
return xerrors.Errorf("fetch aibridge session %q: %w", sessID, err)
}

err = q.auth.Authorize(ctx, act, policy.ActionUpdate, sess.RBACObject())
if err != nil {
return logNotAuthorizedError(ctx, q.log, err)
}

return nil
}

type authContextKey struct{}

// ActorFromContext returns the authorization subject from the context.
Expand Down Expand Up @@ -542,6 +563,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
Expand Down Expand Up @@ -624,6 +668,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",
}
Expand Down Expand Up @@ -1866,6 +1916,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)
}
Expand Down Expand Up @@ -3745,6 +3799,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 session.
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 session.
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 session.
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.
Expand Down
52 changes: 52 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4326,3 +4326,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)
}))
}
35 changes: 35 additions & 0 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading