feat: refine api key permissions and admin routes
This commit is contained in:
@@ -2,9 +2,11 @@ package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
@@ -18,6 +20,21 @@ func (s *Server) listAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) listAPIKeyAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
items, err := s.store.ListAPIKeyAccessRules(r.Context(), user)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrLocalUserRequired) {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.logger.Error("list api key access rules failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list api key access rules failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) createAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.AccessRuleInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
@@ -60,6 +77,38 @@ func (s *Server) batchAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) batchAPIKeyAccessRules(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
var input store.AccessRuleBatchInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid json body")
|
||||
return
|
||||
}
|
||||
if !validAccessRuleBatchInput(input) || input.SubjectType != "api_key" {
|
||||
writeError(w, http.StatusBadRequest, "api key subject, effect and resources are required")
|
||||
return
|
||||
}
|
||||
items, err := s.store.BatchAPIKeyAccessRules(r.Context(), input, user)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrLocalUserRequired) {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "api key not found")
|
||||
return
|
||||
}
|
||||
if errors.Is(err, store.ErrAccessRuleResourceDenied) {
|
||||
writeError(w, http.StatusForbidden, "resource is not available for current user group")
|
||||
return
|
||||
}
|
||||
s.logger.Error("batch api key access rules failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "batch api key access rules failed")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
||||
}
|
||||
|
||||
func (s *Server) updateAccessRule(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.AccessRuleInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
|
||||
@@ -123,6 +123,7 @@ func TestCoreLocalFlow(t *testing.T) {
|
||||
if _, err := testPool.Exec(ctx, `UPDATE gateway_users SET roles = '["admin"]'::jsonb WHERE username = $1`, username); err != nil {
|
||||
t.Fatalf("promote smoke user: %v", err)
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodGet, "/api/admin/models", apiKeyResponse.Secret, nil, http.StatusForbidden, nil)
|
||||
|
||||
inviteCode := "INVITE-" + suffixText
|
||||
if _, err := testPool.Exec(ctx, `
|
||||
@@ -173,7 +174,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
|
||||
PlatformKey string `json:"platformKey"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/v1/platforms", apiKeyResponse.Secret, map[string]any{
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", loginResponse.AccessToken, map[string]any{
|
||||
"provider": "openai",
|
||||
"platformKey": "openai-smoke-" + suffixText,
|
||||
"name": "OpenAI Smoke",
|
||||
@@ -194,7 +195,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
|
||||
ModelType []string `json:"modelType"`
|
||||
} `json:"items"`
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodGet, "/api/v1/catalog/base-models", apiKeyResponse.Secret, nil, http.StatusOK, &baseModels)
|
||||
doJSON(t, server.URL, http.MethodGet, "/api/admin/catalog/base-models", loginResponse.AccessToken, nil, http.StatusOK, &baseModels)
|
||||
if len(baseModels.Items) < 300 {
|
||||
t.Fatalf("server-main seed should include the migrated base model catalog: got %d", len(baseModels.Items))
|
||||
}
|
||||
@@ -213,7 +214,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
|
||||
CanonicalModelKey string `json:"canonicalModelKey"`
|
||||
ModelAlias string `json:"modelAlias"`
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/v1/catalog/base-models", apiKeyResponse.Secret, baseModelInput, http.StatusCreated, &createdBaseModel)
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/admin/catalog/base-models", loginResponse.AccessToken, baseModelInput, http.StatusCreated, &createdBaseModel)
|
||||
if createdBaseModel.ID == "" || createdBaseModel.CanonicalModelKey != baseModelInput["canonicalModelKey"] {
|
||||
t.Fatalf("unexpected created base model: %+v", createdBaseModel)
|
||||
}
|
||||
@@ -221,11 +222,11 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
|
||||
var updatedBaseModel struct {
|
||||
ModelAlias string `json:"modelAlias"`
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodPatch, "/api/v1/catalog/base-models/"+createdBaseModel.ID, apiKeyResponse.Secret, baseModelInput, http.StatusOK, &updatedBaseModel)
|
||||
doJSON(t, server.URL, http.MethodPatch, "/api/admin/catalog/base-models/"+createdBaseModel.ID, loginResponse.AccessToken, baseModelInput, http.StatusOK, &updatedBaseModel)
|
||||
if updatedBaseModel.ModelAlias != "Smoke Base Model Updated" {
|
||||
t.Fatalf("unexpected updated base model: %+v", updatedBaseModel)
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodDelete, "/api/v1/catalog/base-models/"+createdBaseModel.ID, apiKeyResponse.Secret, nil, http.StatusNoContent, nil)
|
||||
doJSON(t, server.URL, http.MethodDelete, "/api/admin/catalog/base-models/"+createdBaseModel.ID, loginResponse.AccessToken, nil, http.StatusNoContent, nil)
|
||||
|
||||
var taskResponse struct {
|
||||
Task struct {
|
||||
@@ -321,7 +322,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
|
||||
var failedPlatform struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/v1/platforms", apiKeyResponse.Secret, map[string]any{
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", loginResponse.AccessToken, map[string]any{
|
||||
"provider": "openai",
|
||||
"platformKey": "openai-fail-" + suffixText,
|
||||
"name": "OpenAI Retryable Failure",
|
||||
@@ -333,7 +334,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
|
||||
var successPlatform struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/v1/platforms", apiKeyResponse.Secret, map[string]any{
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", loginResponse.AccessToken, map[string]any{
|
||||
"provider": "openai",
|
||||
"platformKey": "openai-success-" + suffixText,
|
||||
"name": "OpenAI Retry Success",
|
||||
@@ -344,7 +345,7 @@ VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
|
||||
}, http.StatusCreated, &successPlatform)
|
||||
for _, platformID := range []string{failedPlatform.ID, successPlatform.ID} {
|
||||
var platformModel map[string]any
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/v1/platforms/"+platformID+"/models", apiKeyResponse.Secret, map[string]any{
|
||||
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms/"+platformID+"/models", loginResponse.AccessToken, map[string]any{
|
||||
"canonicalModelKey": "openai:gpt-4o-mini",
|
||||
"modelName": failoverModel,
|
||||
"modelAlias": failoverModel,
|
||||
|
||||
@@ -142,6 +142,33 @@ func (s *Server) listPlatforms(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": platforms})
|
||||
}
|
||||
|
||||
func (s *Server) listPlayablePlatforms(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
models, err := s.store.ListAccessiblePlatformModels(r.Context(), user)
|
||||
if err != nil {
|
||||
s.logger.Error("list playable platform models failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list playable platforms failed")
|
||||
return
|
||||
}
|
||||
allowedPlatformIDs := map[string]bool{}
|
||||
for _, model := range models {
|
||||
allowedPlatformIDs[model.PlatformID] = true
|
||||
}
|
||||
platforms, err := s.store.ListPlatforms(r.Context())
|
||||
if err != nil {
|
||||
s.logger.Error("list platforms failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "list playable platforms failed")
|
||||
return
|
||||
}
|
||||
filtered := platforms[:0]
|
||||
for _, platform := range platforms {
|
||||
if platform.Status == "enabled" && allowedPlatformIDs[platform.ID] {
|
||||
filtered = append(filtered, platform)
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"items": filtered})
|
||||
}
|
||||
|
||||
func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
|
||||
var input store.CreatePlatformInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
||||
@@ -406,6 +433,25 @@ func (s *Server) disableAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "disable api key failed")
|
||||
}
|
||||
|
||||
func (s *Server) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
err := s.store.DeleteAPIKey(r.Context(), r.PathValue("apiKeyID"), user)
|
||||
if err == nil {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, store.ErrLocalUserRequired) {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if store.IsNotFound(err) {
|
||||
writeError(w, http.StatusNotFound, "api key not found")
|
||||
return
|
||||
}
|
||||
s.logger.Error("delete api key failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "delete api key failed")
|
||||
}
|
||||
|
||||
func (s *Server) estimatePricing(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
var body map[string]any
|
||||
|
||||
@@ -38,58 +38,63 @@ func NewServer(cfg config.Config, db *store.Store, logger *slog.Logger) http.Han
|
||||
mux.Handle("GET /api/v1/me", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.me)))
|
||||
mux.Handle("GET /api/v1/public/catalog/providers", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listCatalogProviders)))
|
||||
mux.Handle("GET /api/v1/public/catalog/base-models", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listBaseModels)))
|
||||
mux.Handle("GET /api/v1/catalog/providers", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders)))
|
||||
mux.Handle("POST /api/v1/catalog/providers", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createCatalogProvider)))
|
||||
mux.Handle("PATCH /api/v1/catalog/providers/{providerID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateCatalogProvider)))
|
||||
mux.Handle("DELETE /api/v1/catalog/providers/{providerID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteCatalogProvider)))
|
||||
mux.Handle("GET /api/v1/catalog/base-models", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listBaseModels)))
|
||||
mux.Handle("POST /api/v1/catalog/base-models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createBaseModel)))
|
||||
mux.Handle("POST /api/v1/catalog/base-models/reset-all", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.resetAllBaseModels)))
|
||||
mux.Handle("PATCH /api/v1/catalog/base-models/{baseModelID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateBaseModel)))
|
||||
mux.Handle("POST /api/v1/catalog/base-models/{baseModelID}/reset", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.resetBaseModel)))
|
||||
mux.Handle("DELETE /api/v1/catalog/base-models/{baseModelID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteBaseModel)))
|
||||
mux.Handle("GET /api/v1/tenants", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listTenants)))
|
||||
mux.Handle("POST /api/v1/tenants", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createTenant)))
|
||||
mux.Handle("PATCH /api/v1/tenants/{tenantID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateTenant)))
|
||||
mux.Handle("DELETE /api/v1/tenants/{tenantID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteTenant)))
|
||||
mux.Handle("GET /api/v1/users", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listUsers)))
|
||||
mux.Handle("POST /api/v1/users", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createGatewayUser)))
|
||||
mux.Handle("PATCH /api/v1/users/{userID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateGatewayUser)))
|
||||
mux.Handle("DELETE /api/v1/users/{userID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteGatewayUser)))
|
||||
mux.Handle("GET /api/v1/user-groups", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listUserGroups)))
|
||||
mux.Handle("POST /api/v1/user-groups", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createUserGroup)))
|
||||
mux.Handle("PATCH /api/v1/user-groups/{groupID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateUserGroup)))
|
||||
mux.Handle("DELETE /api/v1/user-groups/{groupID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteUserGroup)))
|
||||
mux.Handle("GET /api/v1/access-rules", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listAccessRules)))
|
||||
mux.Handle("POST /api/v1/access-rules", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createAccessRule)))
|
||||
mux.Handle("POST /api/v1/access-rules/batch", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.batchAccessRules)))
|
||||
mux.Handle("PATCH /api/v1/access-rules/{ruleID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateAccessRule)))
|
||||
mux.Handle("DELETE /api/v1/access-rules/{ruleID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteAccessRule)))
|
||||
mux.Handle("GET /api/admin/catalog/providers", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders)))
|
||||
mux.Handle("POST /api/admin/catalog/providers", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createCatalogProvider)))
|
||||
mux.Handle("PATCH /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateCatalogProvider)))
|
||||
mux.Handle("DELETE /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteCatalogProvider)))
|
||||
mux.Handle("GET /api/admin/catalog/base-models", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listBaseModels)))
|
||||
mux.Handle("POST /api/admin/catalog/base-models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createBaseModel)))
|
||||
mux.Handle("POST /api/admin/catalog/base-models/reset-all", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.resetAllBaseModels)))
|
||||
mux.Handle("PATCH /api/admin/catalog/base-models/{baseModelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateBaseModel)))
|
||||
mux.Handle("POST /api/admin/catalog/base-models/{baseModelID}/reset", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.resetBaseModel)))
|
||||
mux.Handle("DELETE /api/admin/catalog/base-models/{baseModelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteBaseModel)))
|
||||
mux.Handle("GET /api/admin/tenants", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listTenants)))
|
||||
mux.Handle("POST /api/admin/tenants", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createTenant)))
|
||||
mux.Handle("PATCH /api/admin/tenants/{tenantID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateTenant)))
|
||||
mux.Handle("DELETE /api/admin/tenants/{tenantID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteTenant)))
|
||||
mux.Handle("GET /api/admin/users", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listUsers)))
|
||||
mux.Handle("POST /api/admin/users", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createGatewayUser)))
|
||||
mux.Handle("PATCH /api/admin/users/{userID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateGatewayUser)))
|
||||
mux.Handle("DELETE /api/admin/users/{userID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteGatewayUser)))
|
||||
mux.Handle("GET /api/admin/user-groups", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listUserGroups)))
|
||||
mux.Handle("POST /api/admin/user-groups", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createUserGroup)))
|
||||
mux.Handle("PATCH /api/admin/user-groups/{groupID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateUserGroup)))
|
||||
mux.Handle("DELETE /api/admin/user-groups/{groupID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteUserGroup)))
|
||||
mux.Handle("GET /api/admin/access-rules", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listAccessRules)))
|
||||
mux.Handle("POST /api/admin/access-rules", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createAccessRule)))
|
||||
mux.Handle("POST /api/admin/access-rules/batch", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.batchAccessRules)))
|
||||
mux.Handle("PATCH /api/admin/access-rules/{ruleID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateAccessRule)))
|
||||
mux.Handle("DELETE /api/admin/access-rules/{ruleID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteAccessRule)))
|
||||
mux.Handle("GET /api/v1/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeys)))
|
||||
mux.Handle("POST /api/v1/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.createAPIKey)))
|
||||
mux.Handle("GET /api/v1/api-keys/access-rules", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeyAccessRules)))
|
||||
mux.Handle("POST /api/v1/api-keys/access-rules/batch", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.batchAPIKeyAccessRules)))
|
||||
mux.Handle("PATCH /api/v1/api-keys/{apiKeyID}/disable", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.disableAPIKey)))
|
||||
mux.Handle("DELETE /api/v1/api-keys/{apiKeyID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.deleteAPIKey)))
|
||||
mux.Handle("GET /api/playground/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableAPIKeys)))
|
||||
mux.Handle("GET /api/v1/pricing/rules", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPricingRules)))
|
||||
mux.Handle("GET /api/v1/pricing/rule-sets", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPricingRuleSets)))
|
||||
mux.Handle("POST /api/v1/pricing/rule-sets", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPricingRuleSet)))
|
||||
mux.Handle("PATCH /api/v1/pricing/rule-sets/{ruleSetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updatePricingRuleSet)))
|
||||
mux.Handle("DELETE /api/v1/pricing/rule-sets/{ruleSetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deletePricingRuleSet)))
|
||||
mux.Handle("GET /api/admin/pricing/rules", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPricingRules)))
|
||||
mux.Handle("GET /api/admin/pricing/rule-sets", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPricingRuleSets)))
|
||||
mux.Handle("POST /api/admin/pricing/rule-sets", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPricingRuleSet)))
|
||||
mux.Handle("PATCH /api/admin/pricing/rule-sets/{ruleSetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updatePricingRuleSet)))
|
||||
mux.Handle("DELETE /api/admin/pricing/rule-sets/{ruleSetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePricingRuleSet)))
|
||||
mux.Handle("POST /api/v1/pricing/estimate", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.estimatePricing)))
|
||||
mux.Handle("GET /api/v1/runtime/policy-sets", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listRuntimePolicySets)))
|
||||
mux.Handle("POST /api/v1/runtime/policy-sets", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createRuntimePolicySet)))
|
||||
mux.Handle("PATCH /api/v1/runtime/policy-sets/{policySetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updateRuntimePolicySet)))
|
||||
mux.Handle("DELETE /api/v1/runtime/policy-sets/{policySetID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deleteRuntimePolicySet)))
|
||||
mux.Handle("GET /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPlatforms)))
|
||||
mux.Handle("POST /api/v1/platforms", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPlatform)))
|
||||
mux.Handle("PATCH /api/v1/platforms/{platformID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.updatePlatform)))
|
||||
mux.Handle("DELETE /api/v1/platforms/{platformID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deletePlatform)))
|
||||
mux.Handle("PUT /api/v1/platforms/{platformID}/models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.replacePlatformModels)))
|
||||
mux.Handle("POST /api/v1/platforms/{platformID}/models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
|
||||
mux.Handle("POST /api/v1/platform-models", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
|
||||
mux.Handle("DELETE /api/v1/platform-models/{modelID}", server.auth.Require(auth.PermissionManager, http.HandlerFunc(server.deletePlatformModel)))
|
||||
mux.Handle("GET /api/v1/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listModels)))
|
||||
mux.Handle("GET /api/admin/runtime/policy-sets", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listRuntimePolicySets)))
|
||||
mux.Handle("POST /api/admin/runtime/policy-sets", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createRuntimePolicySet)))
|
||||
mux.Handle("PATCH /api/admin/runtime/policy-sets/{policySetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateRuntimePolicySet)))
|
||||
mux.Handle("DELETE /api/admin/runtime/policy-sets/{policySetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteRuntimePolicySet)))
|
||||
mux.Handle("GET /api/admin/platforms", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPlatforms)))
|
||||
mux.Handle("POST /api/admin/platforms", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatform)))
|
||||
mux.Handle("PATCH /api/admin/platforms/{platformID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updatePlatform)))
|
||||
mux.Handle("DELETE /api/admin/platforms/{platformID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePlatform)))
|
||||
mux.Handle("PUT /api/admin/platforms/{platformID}/models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.replacePlatformModels)))
|
||||
mux.Handle("POST /api/admin/platforms/{platformID}/models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
|
||||
mux.Handle("POST /api/admin/platform-models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
|
||||
mux.Handle("DELETE /api/admin/platform-models/{modelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePlatformModel)))
|
||||
mux.Handle("GET /api/admin/models", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listModels)))
|
||||
mux.Handle("GET /api/v1/platforms", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayablePlatforms)))
|
||||
mux.Handle("GET /api/v1/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableModels)))
|
||||
mux.Handle("GET /api/v1/playground/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableModels)))
|
||||
mux.Handle("GET /api/v1/runtime/rate-limit-windows", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows)))
|
||||
mux.Handle("GET /api/admin/runtime/rate-limit-windows", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows)))
|
||||
mux.Handle("POST /api/v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions", false)))
|
||||
mux.Handle("POST /api/v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", false)))
|
||||
mux.Handle("POST /api/v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", false)))
|
||||
@@ -109,6 +114,17 @@ func NewServer(cfg config.Config, db *store.Store, logger *slog.Logger) http.Han
|
||||
return server.recover(server.cors(mux))
|
||||
}
|
||||
|
||||
func (s *Server) requireAdmin(permission auth.Permission, next http.Handler) http.Handler {
|
||||
return s.auth.Require(permission, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := auth.UserFromContext(r.Context())
|
||||
if user != nil && strings.TrimSpace(user.APIKeyID) != "" {
|
||||
writeError(w, http.StatusForbidden, "admin api does not accept api key credentials")
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *Server) cors(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
|
||||
@@ -3,6 +3,7 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
|
||||
@@ -66,6 +67,35 @@ ORDER BY resource_type ASC, priority ASC, subject_type ASC, created_at DESC`)
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) ListAPIKeyAccessRules(ctx context.Context, user *auth.User) ([]AccessRule, error) {
|
||||
gatewayUserID := localGatewayUserID(user)
|
||||
if gatewayUserID == "" {
|
||||
return nil, ErrLocalUserRequired
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT `+apiKeyAccessRuleColumns+`
|
||||
FROM gateway_access_rules ar
|
||||
JOIN gateway_api_keys k ON k.id = ar.subject_id
|
||||
WHERE ar.subject_type = 'api_key'
|
||||
AND k.gateway_user_id = $1::uuid
|
||||
AND k.deleted_at IS NULL
|
||||
ORDER BY ar.resource_type ASC, ar.priority ASC, ar.created_at DESC`, gatewayUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]AccessRule, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanAccessRule(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) CreateAccessRule(ctx context.Context, input AccessRuleInput) (AccessRule, error) {
|
||||
input = normalizeAccessRuleInput(input)
|
||||
conditions, _ := json.Marshal(emptyObjectIfNil(input.Conditions))
|
||||
@@ -191,6 +221,38 @@ DO UPDATE SET priority = EXCLUDED.priority,
|
||||
return s.ListAccessRules(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) BatchAPIKeyAccessRules(ctx context.Context, input AccessRuleBatchInput, user *auth.User) ([]AccessRule, error) {
|
||||
gatewayUserID := localGatewayUserID(user)
|
||||
if gatewayUserID == "" {
|
||||
return nil, ErrLocalUserRequired
|
||||
}
|
||||
input = normalizeAccessRuleBatchInput(input)
|
||||
if input.SubjectType != "api_key" {
|
||||
return nil, pgx.ErrNoRows
|
||||
}
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM gateway_api_keys
|
||||
WHERE id = $1::uuid
|
||||
AND gateway_user_id = $2::uuid
|
||||
AND deleted_at IS NULL
|
||||
)`, input.SubjectID, gatewayUserID).Scan(&exists); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, pgx.ErrNoRows
|
||||
}
|
||||
if err := s.ensureAPIKeyAccessRuleResourcesAllowed(ctx, user, input.UpsertResources); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := s.BatchAccessRules(ctx, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.ListAPIKeyAccessRules(ctx, user)
|
||||
}
|
||||
|
||||
func (s *Store) filterCandidatesByAccessRules(ctx context.Context, user *auth.User, candidates []RuntimeModelCandidate) ([]RuntimeModelCandidate, error) {
|
||||
if len(candidates) == 0 {
|
||||
return candidates, nil
|
||||
@@ -221,6 +283,10 @@ func (s *Store) filterCandidatesByAccessRules(ctx context.Context, user *auth.Us
|
||||
}
|
||||
|
||||
func (s *Store) ListAccessiblePlatformModels(ctx context.Context, user *auth.User) ([]PlatformModel, error) {
|
||||
accessUser, err := s.resolveCurrentAccessUser(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models, err := s.ListModels(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -241,7 +307,80 @@ func (s *Store) ListAccessiblePlatformModels(ctx context.Context, user *auth.Use
|
||||
enabled = append(enabled, model)
|
||||
}
|
||||
}
|
||||
return s.filterPlatformModelsByAccessRules(ctx, user, enabled)
|
||||
return s.filterPlatformModelsByAccessRules(ctx, accessUser, enabled)
|
||||
}
|
||||
|
||||
func (s *Store) ensureAPIKeyAccessRuleResourcesAllowed(ctx context.Context, user *auth.User, resources []AccessRuleResourceInput) error {
|
||||
resources = dedupeAccessRuleResources(resources)
|
||||
if len(resources) == 0 {
|
||||
return nil
|
||||
}
|
||||
allowed, err := s.accessibleAccessRuleResources(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, resource := range resources {
|
||||
if !allowed[resource.ResourceType+":"+resource.ResourceID] {
|
||||
return ErrAccessRuleResourceDenied
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) accessibleAccessRuleResources(ctx context.Context, user *auth.User) (map[string]bool, error) {
|
||||
models, err := s.ListAccessiblePlatformModels(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, model := range models {
|
||||
allowed["platform:"+model.PlatformID] = true
|
||||
allowed["platform_model:"+model.ID] = true
|
||||
if model.BaseModelID != "" {
|
||||
allowed["base_model:"+model.BaseModelID] = true
|
||||
}
|
||||
}
|
||||
return allowed, nil
|
||||
}
|
||||
|
||||
func (s *Store) resolveCurrentAccessUser(ctx context.Context, user *auth.User) (*auth.User, error) {
|
||||
if user == nil {
|
||||
return nil, nil
|
||||
}
|
||||
gatewayUserID := localGatewayUserID(user)
|
||||
if gatewayUserID == "" {
|
||||
return user, nil
|
||||
}
|
||||
next := *user
|
||||
var userGroupID string
|
||||
var err error
|
||||
if strings.TrimSpace(user.APIKeyID) != "" {
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(k.user_group_id::text, u.default_user_group_id::text, '')
|
||||
FROM gateway_users u
|
||||
JOIN gateway_api_keys k ON k.gateway_user_id = u.id
|
||||
WHERE u.id = $1::uuid
|
||||
AND k.id = $2::uuid
|
||||
AND u.status = 'active'
|
||||
AND u.deleted_at IS NULL
|
||||
AND k.status = 'active'
|
||||
AND k.deleted_at IS NULL`, gatewayUserID, user.APIKeyID).Scan(&userGroupID)
|
||||
} else {
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(default_user_group_id::text, '')
|
||||
FROM gateway_users
|
||||
WHERE id = $1::uuid
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL`, gatewayUserID).Scan(&userGroupID)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return &next, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
next.UserGroupID = userGroupID
|
||||
return &next, nil
|
||||
}
|
||||
|
||||
func (s *Store) filterPlatformModelsByAccessRules(ctx context.Context, user *auth.User, models []PlatformModel) ([]PlatformModel, error) {
|
||||
@@ -432,6 +571,10 @@ const accessRuleColumns = `
|
||||
id::text, subject_type, subject_id::text, resource_type, resource_id::text, effect,
|
||||
priority, min_permission_level, conditions, metadata, status, created_at, updated_at`
|
||||
|
||||
const apiKeyAccessRuleColumns = `
|
||||
ar.id::text, ar.subject_type, ar.subject_id::text, ar.resource_type, ar.resource_id::text, ar.effect,
|
||||
ar.priority, ar.min_permission_level, ar.conditions, ar.metadata, ar.status, ar.created_at, ar.updated_at`
|
||||
|
||||
func scanAccessRule(row scanner) (AccessRule, error) {
|
||||
var item AccessRule
|
||||
var conditions []byte
|
||||
|
||||
@@ -258,14 +258,25 @@ RETURNING `+userGroupColumns,
|
||||
}
|
||||
|
||||
func (s *Store) DeleteUserGroup(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM gateway_user_groups WHERE id = $1::uuid`, id)
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM gateway_access_rules
|
||||
WHERE subject_type = 'user_group' AND subject_id = $1::uuid`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := tx.Exec(ctx, `DELETE FROM gateway_user_groups WHERE id = $1::uuid`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
const tenantColumns = `
|
||||
|
||||
@@ -48,13 +48,27 @@ func (s *Store) ReplacePlatformModels(ctx context.Context, platformID string, in
|
||||
}
|
||||
|
||||
if len(keptIDs) == 0 {
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM platform_models WHERE platform_id = $1::uuid`, platformID); err != nil {
|
||||
if _, err := tx.Exec(ctx, `
|
||||
WITH deleted AS (
|
||||
DELETE FROM platform_models
|
||||
WHERE platform_id = $1::uuid
|
||||
RETURNING id
|
||||
)
|
||||
DELETE FROM gateway_access_rules
|
||||
WHERE resource_type = 'platform_model'
|
||||
AND resource_id IN (SELECT id FROM deleted)`, platformID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if _, err := tx.Exec(ctx, `
|
||||
DELETE FROM platform_models
|
||||
WHERE platform_id = $1::uuid
|
||||
AND NOT (id::text = ANY($2::text[]))`, platformID, keptIDs); err != nil {
|
||||
WITH deleted AS (
|
||||
DELETE FROM platform_models
|
||||
WHERE platform_id = $1::uuid
|
||||
AND NOT (id::text = ANY($2::text[]))
|
||||
RETURNING id
|
||||
)
|
||||
DELETE FROM gateway_access_rules
|
||||
WHERE resource_type = 'platform_model'
|
||||
AND resource_id IN (SELECT id FROM deleted)`, platformID, keptIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -224,14 +238,25 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
}
|
||||
|
||||
func (s *Store) DeletePlatformModel(ctx context.Context, id string) error {
|
||||
result, err := s.pool.Exec(ctx, `DELETE FROM platform_models WHERE id = $1::uuid`, id)
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
result, err := tx.Exec(ctx, `DELETE FROM platform_models 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_model' AND resource_id = $1::uuid`, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) lookupBaseModel(ctx context.Context, q platformModelQuerier, id string, canonicalKey string, modelName string) (modelCatalogSnapshot, error) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user