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
52 changes: 52 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,27 @@ var (
}.WithCachedASTValue()
}

subjectChatdKeyMinter = func(userID uuid.UUID) rbac.Subject {
return rbac.Subject{
Type: rbac.SubjectTypeChatdKeyMinter,
FriendlyName: "Chatd Key Minter",
ID: userID.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "chatdkeyminter"},
DisplayName: "Chatd Key Minter",
Site: []rbac.Permission{},
User: rbac.Permissions(map[string][]policy.Action{
rbac.ResourceApiKey.Type: {policy.ActionRead, policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceUser.Type: {policy.ActionReadPersonal},
}),
ByOrgID: map[string]rbac.OrgPermissions{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()
}

subjectSystemRestricted = rbac.Subject{
Type: rbac.SubjectTypeSystemRestricted,
FriendlyName: "System",
Expand Down Expand Up @@ -873,6 +894,12 @@ func AsAPIKeyRevoker(ctx context.Context, userID uuid.UUID) context.Context {
return As(ctx, subjectAPIKeyRevoker(userID))
}

// AsChatdKeyMinter returns a context with an actor that manages the synthetic
// gateway API key owned by the specified user.
func AsChatdKeyMinter(ctx context.Context, userID uuid.UUID) context.Context {
return As(ctx, subjectChatdKeyMinter(userID))
}

// AsSystemRestricted returns a context with an actor that has permissions
// required for various system operations (login, logout, metrics cache).
// DO NOT USE THIS UNLESS YOU HAVE ABSOLUTELY NO OTHER CHOICE. Prefer using a
Expand Down Expand Up @@ -3481,6 +3508,13 @@ func (q *querier) GetChatStreamSyncRows(ctx context.Context, ids []uuid.UUID) ([
return q.db.GetChatStreamSyncRows(ctx, ids)
}

func (q *querier) GetChatSyntheticAPIKeyByUserID(ctx context.Context, userID uuid.UUID) (database.ChatSyntheticApiKey, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceApiKey.WithOwner(userID.String())); err != nil {
return database.ChatSyntheticApiKey{}, err
}
return q.db.GetChatSyntheticAPIKeyByUserID(ctx, userID)
}

func (q *querier) GetChatSystemPrompt(ctx context.Context) (string, error) {
// The system prompt is a deployment-wide setting read during chat
// creation by every authenticated user, so no RBAC policy check
Expand Down Expand Up @@ -5059,6 +5093,10 @@ func (q *querier) GetUserCount(ctx context.Context, includeSystem bool) (int64,
return q.db.GetUserCount(ctx, includeSystem)
}

func (q *querier) GetUserForChatSyntheticAPIKeyByID(ctx context.Context, id uuid.UUID) (database.User, error) {
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetUserForChatSyntheticAPIKeyByID)(ctx, id)
}

func (q *querier) GetUserGroupSpendLimit(ctx context.Context, arg database.GetUserGroupSpendLimitParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(arg.UserID.String())); err != nil {
return 0, err
Expand Down Expand Up @@ -5988,6 +6026,13 @@ func (q *querier) InsertChatQueuedMessageWithCreator(ctx context.Context, arg da
return q.db.InsertChatQueuedMessageWithCreator(ctx, arg)
}

func (q *querier) InsertChatSyntheticAPIKey(ctx context.Context, arg database.InsertChatSyntheticAPIKeyParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceApiKey.WithOwner(arg.UserID.String())); err != nil {
return 0, err
}
return q.db.InsertChatSyntheticAPIKey(ctx, arg)
}

func (q *querier) InsertCryptoKey(ctx context.Context, arg database.InsertCryptoKeyParams) (database.CryptoKey, error) {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceCryptoKey); err != nil {
return database.CryptoKey{}, err
Expand Down Expand Up @@ -7372,6 +7417,13 @@ func (q *querier) UpdateChatStatus(ctx context.Context, arg database.UpdateChatS
return q.db.UpdateChatStatus(ctx, arg)
}

func (q *querier) UpdateChatSyntheticAPIKey(ctx context.Context, arg database.UpdateChatSyntheticAPIKeyParams) (int64, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceApiKey.WithOwner(arg.UserID.String())); err != nil {
return 0, err
}
return q.db.UpdateChatSyntheticAPIKey(ctx, arg)
}

func (q *querier) UpdateChatTitleByID(ctx context.Context, arg database.UpdateChatTitleByIDParams) (database.Chat, error) {
chat, err := q.db.GetChatByID(ctx, arg.ID)
if err != nil {
Expand Down
44 changes: 44 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,32 @@ func defaultIPAddress() pqtype.Inet {
}
}

func (s *MethodTestSuite) TestChatSyntheticAPIKey() {
s.Run("GetUserForChatSyntheticAPIKeyByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
user := testutil.Fake(s.T(), faker, database.User{})
dbm.EXPECT().GetUserForChatSyntheticAPIKeyByID(gomock.Any(), user.ID).Return(user, nil).AnyTimes()
check.Args(user.ID).Asserts(user, policy.ActionReadPersonal).Returns(user)
}))
s.Run("GetChatSyntheticAPIKeyByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
user := testutil.Fake(s.T(), faker, database.User{})
mapping := testutil.Fake(s.T(), faker, database.ChatSyntheticApiKey{UserID: user.ID})
dbm.EXPECT().GetChatSyntheticAPIKeyByUserID(gomock.Any(), user.ID).Return(mapping, nil).AnyTimes()
check.Args(user.ID).Asserts(rbac.ResourceApiKey.WithOwner(user.ID.String()), policy.ActionRead).Returns(mapping)
}))
s.Run("InsertChatSyntheticAPIKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
user := testutil.Fake(s.T(), faker, database.User{})
arg := database.InsertChatSyntheticAPIKeyParams{UserID: user.ID, APIKeyID: uuid.NewString()}
dbm.EXPECT().InsertChatSyntheticAPIKey(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceApiKey.WithOwner(user.ID.String()), policy.ActionCreate).Returns(int64(1))
}))
s.Run("UpdateChatSyntheticAPIKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
user := testutil.Fake(s.T(), faker, database.User{})
arg := database.UpdateChatSyntheticAPIKeyParams{UserID: user.ID, OldApiKeyID: uuid.NewString(), NewApiKeyID: uuid.NewString()}
dbm.EXPECT().UpdateChatSyntheticAPIKey(gomock.Any(), arg).Return(int64(1), nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceApiKey.WithOwner(user.ID.String()), policy.ActionUpdate).Returns(int64(1))
}))
}

func (s *MethodTestSuite) TestAPIKey() {
s.Run("DeleteAPIKeyByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
key := testutil.Fake(s.T(), faker, database.APIKey{})
Expand Down Expand Up @@ -7457,6 +7483,24 @@ func TestAsAPIKeyRevoker(t *testing.T) {
})
}

func TestAsChatdKeyMinter(t *testing.T) {
t.Parallel()

userID := uuid.New()
ctx := dbauthz.AsChatdKeyMinter(context.Background(), userID)
actor, ok := dbauthz.ActorFromContext(ctx)
require.True(t, ok)
require.Equal(t, rbac.SubjectTypeChatdKeyMinter, actor.Type)
require.Equal(t, userID.String(), actor.ID)

auth := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
for _, action := range []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete} {
require.NoError(t, auth.Authorize(ctx, actor, action, rbac.ResourceApiKey.WithOwner(userID.String())))
require.Error(t, auth.Authorize(ctx, actor, action, rbac.ResourceApiKey.WithOwner(uuid.NewString())))
}
require.NoError(t, auth.Authorize(ctx, actor, policy.ActionReadPersonal, rbac.ResourceUserObject(userID)))
}

func TestAsChatd(t *testing.T) {
t.Parallel()

Expand Down
32 changes: 32 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.

60 changes: 60 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.

25 changes: 19 additions & 6 deletions coderd/database/dump.sql

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

Loading
Loading