mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add AI Gateway coderd key CRUD endpoints (#25565)
Adds create, list and delete endpoints for AI Gateway keys. Those keys are used to authenticate into Coderd. All endpoints require Owner permission.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package keys
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/apikey"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
const (
|
||||
privateSuffixLength = 32
|
||||
|
||||
// KeyPrefixLength is the total length of the visible key prefix.
|
||||
KeyPrefixLength = 11
|
||||
|
||||
// KeyLength is the total length of the plaintext key returned to
|
||||
// the user on Create.
|
||||
KeyLength = KeyPrefixLength + privateSuffixLength
|
||||
)
|
||||
|
||||
// New generates an AI Gateway key used for authenticating standalone replicas.
|
||||
// Returns InsertParams ready for the database query.
|
||||
func New(name string) (database.InsertAIGatewayKeyParams, string, error) {
|
||||
secret, hashed, err := apikey.GenerateSecret(KeyLength)
|
||||
if err != nil {
|
||||
return database.InsertAIGatewayKeyParams{}, "", xerrors.Errorf("generate secret: %w", err)
|
||||
}
|
||||
if len(secret) != KeyLength {
|
||||
return database.InsertAIGatewayKeyParams{}, "", xerrors.Errorf("generated secret has unexpected length: got %d, want %d", len(secret), KeyLength)
|
||||
}
|
||||
if KeyLength < KeyPrefixLength {
|
||||
return database.InsertAIGatewayKeyParams{}, "", xerrors.Errorf("KeyLength (%d) must be >= KeyPrefixLength (%d)", KeyLength, KeyPrefixLength)
|
||||
}
|
||||
visiblePrefix := secret[:KeyPrefixLength]
|
||||
|
||||
return database.InsertAIGatewayKeyParams{
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
SecretPrefix: visiblePrefix,
|
||||
HashedSecret: hashed,
|
||||
}, secret, nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package keys_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/aibridge/keys"
|
||||
"github.com/coder/coder/v2/coderd/apikey"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params, key, err := keys.New("test-key")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, key, keys.KeyLength)
|
||||
require.Len(t, params.SecretPrefix, keys.KeyPrefixLength)
|
||||
require.Equal(t, key[:keys.KeyPrefixLength], params.SecretPrefix)
|
||||
require.True(t, apikey.ValidateHash(params.HashedSecret, key))
|
||||
require.False(t, apikey.ValidateHash(params.HashedSecret, key[keys.KeyPrefixLength:]))
|
||||
}
|
||||
Generated
+150
@@ -1474,6 +1474,100 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "List AI Gateway keys",
|
||||
"operationId": "list-ai-gateway-keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIGatewayKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Create AI Gateway key",
|
||||
"operationId": "create-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Create AI Gateway key request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys/{key}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Delete AI Gateway key",
|
||||
"operationId": "delete-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Key ID",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/models": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -15048,6 +15142,29 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIGatewayKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"key_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_used_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIProvider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -17582,6 +17699,39 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CreateAIGatewayKeyRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CreateAIGatewayKeyResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CreateAIProviderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+136
@@ -1303,6 +1303,88 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "List AI Gateway keys",
|
||||
"operationId": "list-ai-gateway-keys",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIGatewayKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Create AI Gateway key",
|
||||
"operationId": "create-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Create AI Gateway key request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.CreateAIGatewayKeyResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/keys/{key}": {
|
||||
"delete": {
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Delete AI Gateway key",
|
||||
"operationId": "delete-ai-gateway-key",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Key ID",
|
||||
"name": "key",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/aibridge/models": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
@@ -13440,6 +13522,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIGatewayKey": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"key_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"last_used_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIProvider": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15887,6 +15992,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CreateAIGatewayKeyRequest": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CreateAIGatewayKeyResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_prefix": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.CreateAIProviderRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -14740,13 +14740,13 @@ func TestAIGatewayKeysTableConstraints(t *testing.T) {
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
|
||||
preExsiting := database.InsertAIGatewayKeyParams{
|
||||
preExisting := database.InsertAIGatewayKeyParams{
|
||||
ID: uuid.New(),
|
||||
Name: "name",
|
||||
SecretPrefix: "cgw_test__1",
|
||||
SecretPrefix: "key_test__1",
|
||||
HashedSecret: []byte("first-secret"),
|
||||
}
|
||||
_, err := db.InsertAIGatewayKey(ctx, preExsiting)
|
||||
_, err := db.InsertAIGatewayKey(ctx, preExisting)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
@@ -14757,67 +14757,67 @@ func TestAIGatewayKeysTableConstraints(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "duplicate name",
|
||||
params: aiGatewayKeyParams(preExsiting.Name, "cgw_test002"),
|
||||
params: aiGatewayKeyParams(preExisting.Name, "key_test002"),
|
||||
expectUniqueErr: database.UniqueAiGatewayKeysNameIndex,
|
||||
},
|
||||
{
|
||||
name: "duplicate secret prefix",
|
||||
params: aiGatewayKeyParams("different-key", preExsiting.SecretPrefix),
|
||||
params: aiGatewayKeyParams("different-key", preExisting.SecretPrefix),
|
||||
expectUniqueErr: database.UniqueAiGatewayKeysSecretPrefixIndex,
|
||||
},
|
||||
{
|
||||
name: "duplicate hashed secret",
|
||||
params: database.InsertAIGatewayKeyParams{ID: uuid.New(), Name: "other-name", SecretPrefix: "cgw_1234567", HashedSecret: preExsiting.HashedSecret},
|
||||
params: database.InsertAIGatewayKeyParams{ID: uuid.New(), Name: "other-name", SecretPrefix: "key_1234567", HashedSecret: preExisting.HashedSecret},
|
||||
expectUniqueErr: database.UniqueAiGatewayKeysHashedSecretIndex,
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
params: aiGatewayKeyParams("", "cgw_1234567"),
|
||||
params: aiGatewayKeyParams("", "key_empty__"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysNameCheck,
|
||||
},
|
||||
{
|
||||
name: "name with trailing dash",
|
||||
params: aiGatewayKeyParams("other-name-", "cgw_1234567"),
|
||||
params: aiGatewayKeyParams("other-name-", "key_trail__"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysNameCheck,
|
||||
},
|
||||
{
|
||||
name: "name with consecutive dashes",
|
||||
params: aiGatewayKeyParams("other--name", "cgw_1234567"),
|
||||
params: aiGatewayKeyParams("other--name", "key_consec_"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysNameCheck,
|
||||
},
|
||||
{
|
||||
name: "name with underscore",
|
||||
params: aiGatewayKeyParams("other_name", "cgw_1234567"),
|
||||
params: aiGatewayKeyParams("other_name", "key_undersc"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysNameCheck,
|
||||
},
|
||||
{
|
||||
name: "name with space",
|
||||
params: aiGatewayKeyParams("other name", "cgw_1234567"),
|
||||
params: aiGatewayKeyParams("other name", "key_spacen_"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysNameCheck,
|
||||
},
|
||||
{
|
||||
name: "name with leading dash",
|
||||
params: aiGatewayKeyParams("-other-name", "cgw_1234567"),
|
||||
params: aiGatewayKeyParams("-other-name", "key_leadng_"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysNameCheck,
|
||||
},
|
||||
{
|
||||
name: "name longer than 64 characters",
|
||||
params: aiGatewayKeyParams(strings.Repeat("a", 65), "cgw_1234567"),
|
||||
params: aiGatewayKeyParams(strings.Repeat("a", 65), "key_longna_"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysNameCheck,
|
||||
},
|
||||
{
|
||||
name: "empty secret prefix",
|
||||
params: aiGatewayKeyParams("other-name", ""),
|
||||
params: aiGatewayKeyParams("check-empty-pfx", ""),
|
||||
expectCheckErr: database.CheckAiGatewayKeysSecretPrefixCheck,
|
||||
},
|
||||
{
|
||||
name: "invalid secret prefix length",
|
||||
params: aiGatewayKeyParams("other-name", "cgw_short"),
|
||||
params: aiGatewayKeyParams("check-short-pfx", "key_short"),
|
||||
expectCheckErr: database.CheckAiGatewayKeysSecretPrefixCheck,
|
||||
},
|
||||
{
|
||||
name: "empty hashed secret",
|
||||
params: database.InsertAIGatewayKeyParams{ID: uuid.New(), Name: "other-name", SecretPrefix: "cgw_1234567"},
|
||||
params: database.InsertAIGatewayKeyParams{ID: uuid.New(), Name: "check-empty-hash", SecretPrefix: "key_ehash__", HashedSecret: []byte{}},
|
||||
expectCheckErr: database.CheckAiGatewayKeysHashedSecretCheck,
|
||||
},
|
||||
}
|
||||
@@ -14841,8 +14841,8 @@ func TestAIGatewayKeysQueries(t *testing.T) {
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
first := aiGatewayKeyParams("first-key", "cgw_first__")
|
||||
second := aiGatewayKeyParams("second-key", "cgw_second_")
|
||||
first := aiGatewayKeyParams("first-key", "key_first__")
|
||||
second := aiGatewayKeyParams("second-key", "key_second_")
|
||||
second.HashedSecret = []byte("second-secret")
|
||||
|
||||
firstRow, err := db.InsertAIGatewayKey(ctx, first)
|
||||
@@ -14889,7 +14889,7 @@ func aiGatewayKeyParams(name string, secretPrefix string) database.InsertAIGatew
|
||||
ID: uuid.New(),
|
||||
Name: name,
|
||||
SecretPrefix: secretPrefix,
|
||||
HashedSecret: []byte("secret"),
|
||||
HashedSecret: []byte("secret-" + name + "-" + secretPrefix),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user