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

Skip to content
Merged
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.

21 changes: 21 additions & 0 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ var (
rbac.ResourceOauth2AppSecret.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceChat.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceAIProvider.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
rbac.ResourceAIGatewayKey.Type: {policy.ActionRead, policy.ActionUpdate},
Comment thread
dannykopping marked this conversation as resolved.
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
Expand Down Expand Up @@ -2753,6 +2754,16 @@ func (q *querier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, in
return q.db.GetAIBridgeUserPromptsByInterceptionID(ctx, interceptionID)
}

// Authenticates a standalone AI Gateway replica by its hashed key secret, returning the matched key.
func (q *querier) GetAIGatewayKeyByHashedSecret(ctx context.Context, hashedSecret []byte) (database.AIGatewayKey, error) {
// Standalone AI Gateway has no Coder identity, so this runs under the
// system actor reading the AI Gateway key it authenticates against.
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAIGatewayKey); err != nil {
return database.AIGatewayKey{}, err
}
return q.db.GetAIGatewayKeyByHashedSecret(ctx, hashedSecret)
}

func (q *querier) GetAIModelPriceByProviderModel(ctx context.Context, arg database.GetAIModelPriceByProviderModelParams) (database.AIModelPrice, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAiModelPrice); err != nil {
return database.AIModelPrice{}, err
Expand Down Expand Up @@ -7022,6 +7033,16 @@ func (q *querier) UpdateAIBridgeInterceptionEnded(ctx context.Context, params da
return q.db.UpdateAIBridgeInterceptionEnded(ctx, params)
}

// Records liveness for a key used in active DRPC session between coderd and standalone AI Gateway.
func (q *querier) UpdateAIGatewayKeyLastUsedAt(ctx context.Context, id uuid.UUID) (int64, error) {
// Standalone AI Gateway has no Coder identity, so this runs under the
// system actor recording connection liveness on the AI Gateway key.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIGatewayKey); err != nil {
return 0, err
}
return q.db.UpdateAIGatewayKeyLastUsedAt(ctx, id)
}

func (q *querier) UpdateAIProvider(ctx context.Context, arg database.UpdateAIProviderParams) (database.AIProvider, error) {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAIProvider); err != nil {
return database.AIProvider{}, err
Expand Down
11 changes: 11 additions & 0 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6968,6 +6968,17 @@ func (s *MethodTestSuite) TestAIBridge() {
dbm.EXPECT().DeleteAIGatewayKey(gomock.Any(), id).Return(database.DeleteAIGatewayKeyRow{}, nil).AnyTimes()
check.Args(id).Asserts(rbac.ResourceAIGatewayKey, policy.ActionDelete).Returns(database.DeleteAIGatewayKeyRow{})
}))
s.Run("GetAIGatewayKeyByHashedSecret", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
hashedSecret := []byte("hashed-secret")
key := database.AIGatewayKey{ID: uuid.New(), HashedSecret: hashedSecret}
dbm.EXPECT().GetAIGatewayKeyByHashedSecret(gomock.Any(), hashedSecret).Return(key, nil).AnyTimes()
check.Args(hashedSecret).Asserts(rbac.ResourceAIGatewayKey, policy.ActionRead).Returns(key)
}))
s.Run("UpdateAIGatewayKeyLastUsedAt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
id := uuid.New()
dbm.EXPECT().UpdateAIGatewayKeyLastUsedAt(gomock.Any(), id).Return(int64(1), nil).AnyTimes()
check.Args(id).Asserts(rbac.ResourceAIGatewayKey, policy.ActionUpdate).Returns(int64(1))
}))
}

func (s *MethodTestSuite) TestTelemetry() {
Expand Down
16 changes: 16 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.

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

3 changes: 2 additions & 1 deletion coderd/database/dump.sql

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Enum additions to api_key_scope are intentionally not reverted because
-- Postgres cannot drop enum values safely.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_gateway_key:update';
5 changes: 4 additions & 1 deletion coderd/database/models.go

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

8 changes: 8 additions & 0 deletions coderd/database/querier.go

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

79 changes: 79 additions & 0 deletions coderd/database/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14958,6 +14958,85 @@ func TestAIGatewayKeysQueries(t *testing.T) {
requireAIGatewayKeysRow(t, keys[0], second, secondRow.CreatedAt)
}

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

db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitLong)

first := aiGatewayKeyParams("lookup-first", "key_lookup1")
second := aiGatewayKeyParams("lookup-second", "key_lookup2")

_, err := db.InsertAIGatewayKey(ctx, first)
require.NoError(t, err)
_, err = db.InsertAIGatewayKey(ctx, second)
require.NoError(t, err)

key, err := db.GetAIGatewayKeyByHashedSecret(ctx, first.HashedSecret)
require.NoError(t, err)
require.Equal(t, first.ID, key.ID)
require.Equal(t, first.Name, key.Name)
require.Equal(t, first.SecretPrefix, key.SecretPrefix)
require.Equal(t, first.HashedSecret, key.HashedSecret)

key, err = db.GetAIGatewayKeyByHashedSecret(ctx, second.HashedSecret)
require.NoError(t, err)
require.Equal(t, second.ID, key.ID)

// An unknown secret returns no rows
key, err = db.GetAIGatewayKeyByHashedSecret(ctx, []byte("does-not-exist"))
require.ErrorIs(t, err, sql.ErrNoRows)
require.Empty(t, key.ID)
}

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

db, _, sqlDB := dbtestutil.NewDBWithSQLDB(t)
ctx := testutil.Context(t, testutil.WaitLong)

params := aiGatewayKeyParams("liveness-key", "key_live___")
row, err := db.InsertAIGatewayKey(ctx, params)
require.NoError(t, err)

// last_used_at starts NULL until a session records liveness.
keys, err := db.ListAIGatewayKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
require.False(t, keys[0].LastUsedAt.Valid)

rows, err := db.UpdateAIGatewayKeyLastUsedAt(ctx, params.ID)
require.NoError(t, err)
require.EqualValues(t, 1, rows)

keys, err = db.ListAIGatewayKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
require.True(t, keys[0].LastUsedAt.Valid)
// The database stamps the timestamp, so compare against the row's
// DB-generated CreatedAt to avoid client clock skew.
require.False(t, keys[0].LastUsedAt.Time.Before(row.CreatedAt))

// Updating a key that does not exist is a no-op, not an error.
rows, err = db.UpdateAIGatewayKeyLastUsedAt(ctx, uuid.New())
require.NoError(t, err)
require.EqualValues(t, 0, rows)

// Set last_used_at to old time to confirm the update overwrites it with a fresh timestamp.
staleTime := row.CreatedAt.Add(-time.Hour)
_, err = sqlDB.ExecContext(ctx, "UPDATE ai_gateway_keys SET last_used_at = $1 WHERE id = $2", staleTime, params.ID)
require.NoError(t, err)

rows, err = db.UpdateAIGatewayKeyLastUsedAt(ctx, params.ID)
require.NoError(t, err)
require.EqualValues(t, 1, rows)

keys, err = db.ListAIGatewayKeys(ctx)
require.NoError(t, err)
require.Len(t, keys, 1)
require.True(t, keys[0].LastUsedAt.Time.After(staleTime))
}

func aiGatewayKeyParams(name string, secretPrefix string) database.InsertAIGatewayKeyParams {
return database.InsertAIGatewayKeyParams{
ID: uuid.New(),
Expand Down
40 changes: 40 additions & 0 deletions coderd/database/queries.sql.go

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

16 changes: 16 additions & 0 deletions coderd/database/queries/ai_gateway_keys.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,19 @@ ORDER BY created_at ASC;
-- name: DeleteAIGatewayKey :one
DELETE FROM ai_gateway_keys WHERE id = $1
RETURNING id, name, secret_prefix, created_at, last_used_at;

-- name: GetAIGatewayKeyByHashedSecret :one
-- Authenticates a standalone AI Gateway replica by its hashed key secret,
-- returning the matched key. The lookup is an exact match on a unique index,
-- so a returned row is itself proof the secret is valid.
SELECT *
FROM ai_gateway_keys
WHERE hashed_secret = $1;

-- name: UpdateAIGatewayKeyLastUsedAt :execrows
-- Records liveness for an active Gateway DRPC session. The database sets the
-- timestamp so it stays consistent regardless of clock drift between API
-- replicas.
UPDATE ai_gateway_keys
SET last_used_at = NOW()
WHERE id = $1;
1 change: 1 addition & 0 deletions coderd/rbac/object_gen.go

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

Loading
Loading