AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
package apikey
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"aigateway.local/core/internal/identity"
|
||||
"aigateway.local/core/internal/platform/apiresponse"
|
||||
)
|
||||
|
||||
type AdminHTTPHandler struct {
|
||||
repository *Repository
|
||||
authenticator *Authenticator
|
||||
usage *UsageStore
|
||||
identity *identity.Service
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
type createRequest struct {
|
||||
Name string `json:"name"`
|
||||
Scopes []string `json:"scopes"`
|
||||
RequestsPerMinute int `json:"requests_per_minute"`
|
||||
MonthlyRequestQuota int64 `json:"monthly_request_quota"`
|
||||
MonthlyTokenQuota int64 `json:"monthly_token_quota"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type limitsRequest struct {
|
||||
RequestsPerMinute int `json:"requests_per_minute"`
|
||||
MonthlyRequestQuota int64 `json:"monthly_request_quota"`
|
||||
MonthlyTokenQuota int64 `json:"monthly_token_quota"`
|
||||
}
|
||||
|
||||
func NewAdminHTTPHandler(repository *Repository, authenticator *Authenticator, identityService *identity.Service) *AdminHTTPHandler {
|
||||
h := &AdminHTTPHandler{repository: repository, authenticator: authenticator, identity: identityService, mux: http.NewServeMux()}
|
||||
h.mux.HandleFunc("GET /api/v1/admin/api-keys", h.list)
|
||||
h.mux.HandleFunc("POST /api/v1/admin/api-keys", h.create)
|
||||
h.mux.HandleFunc("PUT /api/v1/admin/api-keys/{api_key_id}/limits", h.updateLimits)
|
||||
h.mux.HandleFunc("DELETE /api/v1/admin/api-keys/{api_key_id}", h.revoke)
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *AdminHTTPHandler) SetUsageStore(store *UsageStore) { h.usage = store }
|
||||
|
||||
func (h *AdminHTTPHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
h.mux.ServeHTTP(writer, request)
|
||||
}
|
||||
|
||||
func (h *AdminHTTPHandler) list(writer http.ResponseWriter, request *http.Request) {
|
||||
if _, ok := h.requirePermission(writer, request, identity.PermissionAPIKeyRead); !ok {
|
||||
return
|
||||
}
|
||||
records, err := h.repository.List(request.Context())
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
ids := make([]string, 0, len(records))
|
||||
for _, record := range records {
|
||||
ids = append(ids, record.ID)
|
||||
}
|
||||
usage, err := h.usage.MonthlyTokens(request.Context(), ids, time.Now())
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
items := make([]map[string]any, 0, len(records))
|
||||
for _, record := range records {
|
||||
item := publicRecord(record)
|
||||
item["monthly_token_usage"] = usage[record.ID]
|
||||
items = append(items, item)
|
||||
}
|
||||
apiresponse.OK(writer, items)
|
||||
}
|
||||
|
||||
func (h *AdminHTTPHandler) updateLimits(writer http.ResponseWriter, request *http.Request) {
|
||||
account, ok := h.requirePermission(writer, request, identity.PermissionAPIKeyManage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input limitsRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1<<20))
|
||||
decoder.DisallowUnknownFields()
|
||||
if decoder.Decode(&input) != nil || !validLimits(input.RequestsPerMinute, input.MonthlyRequestQuota, input.MonthlyTokenQuota) {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "API Key 限流或月配额无效")
|
||||
return
|
||||
}
|
||||
record, hash, err := h.repository.UpdateLimits(request.Context(), request.PathValue("api_key_id"), input.RequestsPerMinute, input.MonthlyRequestQuota, input.MonthlyTokenQuota, account.ID)
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
if err := h.authenticator.Invalidate(request.Context(), hash); err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
response := publicRecord(record)
|
||||
if h.usage != nil {
|
||||
usage, usageErr := h.usage.MonthlyTokens(request.Context(), []string{record.ID}, time.Now())
|
||||
if usageErr == nil {
|
||||
response["monthly_token_usage"] = usage[record.ID]
|
||||
}
|
||||
}
|
||||
apiresponse.OK(writer, response)
|
||||
}
|
||||
|
||||
func validLimits(requestsPerMinute int, monthlyRequestQuota, monthlyTokenQuota int64) bool {
|
||||
return requestsPerMinute >= 0 && requestsPerMinute <= 1_000_000 &&
|
||||
monthlyRequestQuota >= 0 && monthlyRequestQuota <= 1_000_000_000_000 &&
|
||||
monthlyTokenQuota >= 0 && monthlyTokenQuota <= 1_000_000_000_000_000
|
||||
}
|
||||
|
||||
func (h *AdminHTTPHandler) create(writer http.ResponseWriter, request *http.Request) {
|
||||
account, ok := h.requirePermission(writer, request, identity.PermissionAPIKeyManage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var input createRequest
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(writer, request.Body, 1<<20))
|
||||
decoder.DisallowUnknownFields()
|
||||
if decoder.Decode(&input) != nil {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "请求格式无效")
|
||||
return
|
||||
}
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
if input.Name == "" || len(input.Name) > 128 || len(input.Scopes) == 0 || input.ExpiresAt != nil && !input.ExpiresAt.After(time.Now()) {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "名称、权限范围或过期时间无效")
|
||||
return
|
||||
}
|
||||
if !validLimits(input.RequestsPerMinute, input.MonthlyRequestQuota, input.MonthlyTokenQuota) {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "API Key 限流或月配额无效")
|
||||
return
|
||||
}
|
||||
for _, scope := range input.Scopes {
|
||||
if scope != "gateway:invoke" && scope != "*" {
|
||||
apiresponse.Error(writer, http.StatusBadRequest, "包含不支持的权限范围")
|
||||
return
|
||||
}
|
||||
}
|
||||
record, secret, err := h.repository.Create(request.Context(), input.Name, input.Scopes, input.RequestsPerMinute, input.MonthlyRequestQuota, input.MonthlyTokenQuota, input.ExpiresAt, account.ID)
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
response := publicRecord(record)
|
||||
response["key"] = secret
|
||||
response["warning"] = "密钥只显示一次,请立即安全保存"
|
||||
apiresponse.OK(writer, response)
|
||||
}
|
||||
|
||||
func (h *AdminHTTPHandler) revoke(writer http.ResponseWriter, request *http.Request) {
|
||||
account, ok := h.requirePermission(writer, request, identity.PermissionAPIKeyManage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
hash, err := h.repository.Revoke(request.Context(), request.PathValue("api_key_id"), account.ID)
|
||||
if err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
if err := h.authenticator.Invalidate(request.Context(), hash); err != nil {
|
||||
h.writeError(writer, err)
|
||||
return
|
||||
}
|
||||
apiresponse.OK(writer, map[string]bool{"revoked": true})
|
||||
}
|
||||
|
||||
func (h *AdminHTTPHandler) requirePermission(writer http.ResponseWriter, request *http.Request, permission string) (identity.Account, bool) {
|
||||
account, err := h.identity.Authenticate(request.Context(), identity.KindAdmin, request.Header.Get("Authorization"))
|
||||
if err != nil {
|
||||
apiresponse.Error(writer, http.StatusUnauthorized, "登录状态无效或已过期")
|
||||
return identity.Account{}, false
|
||||
}
|
||||
if !identity.HasPermission(account, permission) {
|
||||
apiresponse.Error(writer, http.StatusForbidden, "缺少 API Key 操作权限")
|
||||
return identity.Account{}, false
|
||||
}
|
||||
return account, true
|
||||
}
|
||||
|
||||
func (h *AdminHTTPHandler) writeError(writer http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, ErrInvalid):
|
||||
apiresponse.Error(writer, http.StatusNotFound, "API Key 不存在或已撤销")
|
||||
case errors.Is(err, ErrStore):
|
||||
apiresponse.Error(writer, http.StatusServiceUnavailable, "API Key 服务暂不可用")
|
||||
default:
|
||||
apiresponse.Error(writer, http.StatusInternalServerError, "API Key 处理失败")
|
||||
}
|
||||
}
|
||||
|
||||
func publicRecord(record Record) map[string]any {
|
||||
return map[string]any{
|
||||
"id": record.ID, "name": record.Name, "key_prefix": record.KeyPrefix,
|
||||
"scopes": record.Scopes, "enabled": record.Enabled, "expires_at": record.ExpiresAt,
|
||||
"requests_per_minute": record.RequestsPerMinute, "monthly_request_quota": record.MonthlyRequestQuota,
|
||||
"monthly_token_quota": record.MonthlyTokenQuota,
|
||||
"last_used_at": record.LastUsedAt, "created_at": record.CreatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user