feat: refine api key permissions and admin routes

This commit is contained in:
2026-05-10 23:22:26 +08:00
parent 0fc23d7eb8
commit d86651ff55
23 changed files with 1683 additions and 532 deletions
+84 -11
View File
@@ -22,12 +22,13 @@ type Store struct {
}
var (
ErrInvalidCredentials = errors.New("invalid account or password")
ErrInvalidInvitation = errors.New("invalid or expired invitation code")
ErrLocalUserRequired = errors.New("local gateway user is required")
ErrProtectedDefault = errors.New("protected default resource cannot be deleted")
ErrUserAlreadyExists = errors.New("user already exists")
ErrWeakPassword = errors.New("password must be at least 8 characters")
ErrInvalidCredentials = errors.New("invalid account or password")
ErrInvalidInvitation = errors.New("invalid or expired invitation code")
ErrAccessRuleResourceDenied = errors.New("access rule resource is not available")
ErrLocalUserRequired = errors.New("local gateway user is required")
ErrProtectedDefault = errors.New("protected default resource cannot be deleted")
ErrUserAlreadyExists = errors.New("user already exists")
ErrWeakPassword = errors.New("password must be at least 8 characters")
)
func Connect(ctx context.Context, databaseURL string) (*Store, error) {
@@ -102,6 +103,7 @@ type APIKey struct {
TenantKey string `json:"tenantKey,omitempty"`
UserID string `json:"userId,omitempty"`
KeyPrefix string `json:"keyPrefix"`
Secret string `json:"secret,omitempty"`
Name string `json:"name"`
Scopes []string `json:"scopes,omitempty"`
UserGroupID string `json:"userGroupId,omitempty"`
@@ -627,14 +629,45 @@ RETURNING id::text, provider, platform_key, name, COALESCE(internal_name, ''), C
}
func (s *Store) DeletePlatform(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `DELETE FROM integration_platforms WHERE id = $1::uuid`, id)
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
rows, err := tx.Query(ctx, `SELECT id::text FROM platform_models WHERE platform_id = $1::uuid`, id)
if err != nil {
return err
}
modelIDs := make([]string, 0)
for rows.Next() {
var modelID string
if err := rows.Scan(&modelID); err != nil {
rows.Close()
return err
}
modelIDs = append(modelIDs, modelID)
}
if err := rows.Err(); err != nil {
rows.Close()
return err
}
rows.Close()
result, err := tx.Exec(ctx, `DELETE FROM integration_platforms WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE (resource_type = 'platform' AND resource_id = $1::uuid)
OR (resource_type = 'platform_model' AND resource_id::text = ANY($2::text[]))`, id, modelIDs); err != nil {
return err
}
return tx.Commit(ctx)
}
func (s *Store) ListModels(ctx context.Context) ([]PlatformModel, error) {
@@ -962,7 +995,11 @@ SELECT id::text, COALESCE(gateway_tenant_id::text, ''), gateway_user_id::text,
COALESCE(tenant_id, ''), COALESCE(tenant_key, ''), COALESCE(user_id, ''),
key_prefix, name, scopes, COALESCE(user_group_id::text, ''),
rate_limit_policy, quota_policy, status, COALESCE(expires_at::text, ''),
COALESCE(last_used_at::text, ''), created_at, updated_at
COALESCE(last_used_at::text, ''), created_at, updated_at,
CASE WHEN status = 'active' AND (expires_at IS NULL OR expires_at > now())
THEN COALESCE(key_secret, '')
ELSE ''
END
FROM gateway_api_keys
WHERE gateway_user_id = $1::uuid AND deleted_at IS NULL
ORDER BY created_at DESC`, gatewayUserID)
@@ -973,7 +1010,8 @@ ORDER BY created_at DESC`, gatewayUserID)
items := make([]APIKey, 0)
for rows.Next() {
item, err := scanAPIKey(rows)
var secret string
item, err := scanAPIKeyWithSecret(rows, &secret)
if err != nil {
return nil, err
}
@@ -1155,13 +1193,45 @@ RETURNING id::text, COALESCE(gateway_tenant_id::text, ''), gateway_user_id::text
return item, nil
}
func (s *Store) DeleteAPIKey(ctx context.Context, apiKeyID string, user *auth.User) error {
gatewayUserID := localGatewayUserID(user)
if gatewayUserID == "" {
return ErrLocalUserRequired
}
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
result, err := tx.Exec(ctx, `
UPDATE gateway_api_keys
SET status = 'revoked',
key_secret = NULL,
deleted_at = now(),
updated_at = now()
WHERE id = $1::uuid AND gateway_user_id = $2::uuid AND deleted_at IS NULL`, apiKeyID, gatewayUserID)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = 'api_key' AND subject_id = $1::uuid`, apiKeyID); err != nil {
return err
}
return tx.Commit(ctx)
}
func (s *Store) VerifyLocalAPIKey(ctx context.Context, secret string) (*auth.User, error) {
prefix := apiKeyPrefix(secret)
if prefix == "" {
return nil, auth.ErrUnauthorized
}
rows, err := s.pool.Query(ctx, `
SELECT k.id::text, k.key_hash, k.key_prefix, k.name, COALESCE(k.user_group_id::text, ''),
SELECT k.id::text, k.key_hash, k.key_prefix, k.name, COALESCE(k.user_group_id::text, u.default_user_group_id::text, ''),
u.id::text, u.username, u.roles, COALESCE(u.gateway_tenant_id::text, ''),
COALESCE(u.tenant_id, ''), COALESCE(u.tenant_key, '')
FROM gateway_api_keys k
@@ -1722,6 +1792,9 @@ func scanAPIKeyWithSecret(scanner apiKeyScanner, secret *string) (APIKey, error)
item.Scopes = decodeStringArray(scopesBytes)
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
item.QuotaPolicy = decodeObject(quotaPolicy)
if secret != nil {
item.Secret = *secret
}
return item, nil
}