easyai-ai-gateway/apps/api/internal/httpapi/access_rule_handlers.go
chensipeng 918dfbfee1 docs(api): 补全 OpenAPI 注释与生成文档
为接口、模型与脚本补齐 Swagger/OpenAPI 注释,生成最新文档,并增加一键生成与查看入口。
2026-05-14 18:18:27 +08:00

285 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
)
// listAccessRules godoc
// @Summary 列出访问规则
// @Description 管理端返回用户组、租户、用户或 API Key 到平台、平台模型、基础模型的访问规则。
// @Tags access-rules
// @Produce json
// @Security BearerAuth
// @Success 200 {object} AccessRuleListResponse
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/access-rules [get]
func (s *Server) listAccessRules(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListAccessRules(r.Context())
if err != nil {
s.logger.Error("list access rules failed", "error", err)
writeError(w, http.StatusInternalServerError, "list access rules failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
// listAPIKeyAccessRules godoc
// @Summary 列出 API Key 访问规则
// @Description 返回当前本地用户可管理的 API Key 访问规则。
// @Tags api-keys
// @Produce json
// @Security BearerAuth
// @Success 200 {object} AccessRuleListResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/api-keys/access-rules [get]
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})
}
// createAccessRule godoc
// @Summary 创建访问规则
// @Description 管理端创建一条访问控制规则。
// @Tags access-rules
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body store.AccessRuleInput true "访问规则请求"
// @Success 201 {object} store.AccessRule
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/access-rules [post]
func (s *Server) createAccessRule(w http.ResponseWriter, r *http.Request) {
var input store.AccessRuleInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validAccessRuleInput(input) {
writeError(w, http.StatusBadRequest, "subject, resource and effect are required")
return
}
item, err := s.store.CreateAccessRule(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "access rule already exists")
return
}
s.logger.Error("create access rule failed", "error", err)
writeError(w, http.StatusInternalServerError, "create access rule failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
// batchAccessRules godoc
// @Summary 批量写入访问规则
// @Description 管理端为同一主体批量新增、更新或删除资源访问规则。
// @Tags access-rules
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body store.AccessRuleBatchInput true "访问规则批量请求"
// @Success 200 {object} AccessRuleListResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/access-rules/batch [post]
func (s *Server) batchAccessRules(w http.ResponseWriter, r *http.Request) {
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) {
writeError(w, http.StatusBadRequest, "subject, effect and resources are required")
return
}
items, err := s.store.BatchAccessRules(r.Context(), input)
if err != nil {
s.logger.Error("batch access rules failed", "error", err)
writeError(w, http.StatusInternalServerError, "batch access rules failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
// batchAPIKeyAccessRules godoc
// @Summary 批量写入 API Key 访问规则
// @Description 当前本地用户为自己的 API Key 批量新增、更新或删除可访问资源。
// @Tags api-keys
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param input body store.AccessRuleBatchInput true "API Key 访问规则批量请求subjectType 必须为 api_key"
// @Success 200 {object} AccessRuleListResponse
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/v1/api-keys/access-rules/batch [post]
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})
}
// updateAccessRule godoc
// @Summary 更新访问规则
// @Description 管理端更新一条访问控制规则。
// @Tags access-rules
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param ruleID path string true "访问规则 ID"
// @Param input body store.AccessRuleInput true "访问规则请求"
// @Success 200 {object} store.AccessRule
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/access-rules/{ruleID} [patch]
func (s *Server) updateAccessRule(w http.ResponseWriter, r *http.Request) {
var input store.AccessRuleInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validAccessRuleInput(input) {
writeError(w, http.StatusBadRequest, "subject, resource and effect are required")
return
}
item, err := s.store.UpdateAccessRule(r.Context(), r.PathValue("ruleID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "access rule not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "access rule already exists")
return
}
s.logger.Error("update access rule failed", "error", err)
writeError(w, http.StatusInternalServerError, "update access rule failed")
return
}
writeJSON(w, http.StatusOK, item)
}
// deleteAccessRule godoc
// @Summary 删除访问规则
// @Description 管理端删除一条访问控制规则。
// @Tags access-rules
// @Produce json
// @Security BearerAuth
// @Param ruleID path string true "访问规则 ID"
// @Success 204 "No Content"
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Failure 500 {object} ErrorEnvelope
// @Router /api/admin/access-rules/{ruleID} [delete]
func (s *Server) deleteAccessRule(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteAccessRule(r.Context(), r.PathValue("ruleID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "access rule not found")
return
}
s.logger.Error("delete access rule failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete access rule failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func validAccessRuleInput(input store.AccessRuleInput) bool {
return validOneOf(input.SubjectType, "user_group", "tenant", "user", "api_key") &&
strings.TrimSpace(input.SubjectID) != "" &&
validOneOf(input.ResourceType, "platform", "platform_model", "base_model") &&
strings.TrimSpace(input.ResourceID) != "" &&
validOneOf(input.Effect, "allow", "deny") &&
(input.Status == "" || validOneOf(input.Status, "active", "disabled"))
}
func validAccessRuleBatchInput(input store.AccessRuleBatchInput) bool {
if !validOneOf(input.SubjectType, "user_group", "tenant", "user", "api_key") ||
strings.TrimSpace(input.SubjectID) == "" ||
!validOneOf(input.Effect, "allow", "deny") {
return false
}
if len(input.UpsertResources) == 0 && len(input.DeleteResources) == 0 {
return false
}
for _, resource := range append(input.UpsertResources, input.DeleteResources...) {
if !validOneOf(resource.ResourceType, "platform", "platform_model", "base_model") ||
strings.TrimSpace(resource.ResourceID) == "" ||
(resource.Status != "" && !validOneOf(resource.Status, "active", "disabled")) {
return false
}
}
return true
}
func validOneOf(value string, allowed ...string) bool {
value = strings.TrimSpace(value)
for _, item := range allowed {
if value == item {
return true
}
}
return false
}