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 }