修复全量审查问题;设置接口PATCH化;回传指纹加固
CI / test (push) Successful in 32s

This commit is contained in:
2026-07-22 16:51:23 +08:00
parent 0614ef22af
commit f51fb6c722
66 changed files with 3997 additions and 687 deletions
+138 -3
View File
@@ -1,13 +1,121 @@
package api
import (
"errors"
"io"
"log"
"net/http"
"github.com/gin-gonic/gin"
"oci-portal/internal/oci"
"oci-portal/internal/service"
)
// idpIconResponse 是图标上传响应;fileName 为域存储内标识,留作后续清理。
type idpIconResponse struct {
URL string `json:"url"`
FileName string `json:"fileName"`
}
type idpSetupWarning struct {
Code string `json:"code"`
Message string `json:"message"`
ResourceCreated bool `json:"resourceCreated"`
RequestID string `json:"requestId"`
}
type createIdpResponse struct {
oci.IdentityProviderInfo
SetupWarning *idpSetupWarning `json:"setupWarning,omitempty"`
}
// @Summary 上传身份提供商图标
// @Description 上传到身份域公共图片存储(/storage/v1/Images),返回可填入 iconUrl 的公网地址
// @Tags 租户 IAM
// @Accept multipart/form-data
// @Param id path int true "配置 ID"
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
// @Param file formData file true "图标文件(png/jpg/jpeg/gif/svg/webp/ico,≤1MB)"
// @Success 200 {object} idpIconResponse
// @Failure 400 {object} map[string]string "缺文件字段、文件为空或文件名非法"
// @Failure 413 {object} map[string]string "文件超过 1MB"
// @Failure 415 {object} map[string]string "扩展名不支持或与文件内容不符"
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/idp-icons [post]
func (h *ociConfigHandler) uploadIdpIcon(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
file, header, err := c.Request.FormFile("file")
if err != nil {
status, msg := iconFormStatus(err)
c.JSON(status, gin.H{"error": msg})
return
}
defer file.Close()
data, err := io.ReadAll(file) // 请求体总量由路由级 bodyLimit 兜底
if err != nil {
status, msg := iconFormStatus(err)
c.JSON(status, gin.H{"error": msg})
return
}
url, fileName, err := h.svc.UploadIdpIcon(c.Request.Context(), id, c.Query("domainId"), header.Filename, data)
if err != nil {
respondIdpIconErr(c, err)
return
}
c.JSON(http.StatusOK, idpIconResponse{URL: url, FileName: fileName})
}
// @Summary 删除身份提供商图标
// @Description 使用上传响应中的 fileName 删除身份域公开图片;404 按已删除处理
// @Tags 租户 IAM
// @Param id path int true "配置 ID"
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
// @Param fileName query string true "上传响应返回的 IdP 图标存储标识(images/idp-icon-<32hex>.<ext>)"
// @Success 204 "无内容"
// @Failure 400 {object} map[string]string "fileName 缺失或不是本服务生成的 IdP 图标标识"
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/idp-icons [delete]
func (h *ociConfigHandler) deleteIdpIcon(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
if err := h.svc.DeleteIdpIcon(c.Request.Context(), id, c.Query("domainId"), c.Query("fileName")); err != nil {
respondIdpIconErr(c, err)
return
}
c.Status(http.StatusNoContent)
}
// iconFormStatus 区分请求体超限(413)与表单缺失/损坏(400)。
func iconFormStatus(err error) (int, string) {
var mbe *http.MaxBytesError
if errors.As(err, &mbe) {
return http.StatusRequestEntityTooLarge, "请求体超出上限(图标最大 1MB)"
}
return http.StatusBadRequest, "缺少文件字段 file"
}
// respondIdpIconErr 把图标校验哨兵错误映射为语义状态码,其余走统一错误边界。
func respondIdpIconErr(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrIdpIconTooLarge):
c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "图标不能超过 1MB"})
case errors.Is(err, service.ErrIdpIconBadType):
c.JSON(http.StatusUnsupportedMediaType, gin.H{"error": "扩展名不支持或与文件内容不符"})
case errors.Is(err, service.ErrIdpIconEmpty):
c.JSON(http.StatusBadRequest, gin.H{"error": "文件为空"})
case errors.Is(err, service.ErrIdpIconBadName):
c.JSON(http.StatusBadRequest, gin.H{"error": "图标文件名非法"})
default:
respondError(c, err)
}
}
// ---- FederationSAML IdP 与 sign-on 免 MFA ----
// createIdpRequest 的映射 / JIT 字段全部可缺省:布尔用指针区分「未传」,
@@ -58,11 +166,12 @@ func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
}
// @Summary 创建身份提供商(SAML)
// @Description 创建禁用态 IdP;若 IdP 已创建但 JIT 后置配置失败且回滚无法确认,仍返回 201,并在 setupWarning 中标明部分成功
// @Tags 租户 IAM
// @Param id path int true "配置 ID"
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
// @Param body body createIdpRequest true "请求体"
// @Success 201 {object} oci.IdentityProviderInfo
// @Success 201 {object} createIdpResponse
// @Security BearerAuth
// @Router /api/v1/oci-configs/{id}/identity-providers [post]
func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
@@ -89,11 +198,37 @@ func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
JitAssignAdminGroup: boolOr(req.JitAssignAdminGroup, true),
JitMapEmail: req.JitMapEmail,
})
if err != nil {
respondCreateIdpResult(c, idp, err)
}
func respondCreateIdpResult(c *gin.Context, idp oci.IdentityProviderInfo, err error) {
if err == nil {
c.JSON(http.StatusCreated, createIdpResponse{IdentityProviderInfo: idp})
return
}
var partial *oci.PartialIdentityProviderCreateError
if !errors.As(err, &partial) {
respondError(c, err)
return
}
c.JSON(http.StatusCreated, idp)
requestID := logPartialIdpCreate(c, partial)
c.JSON(http.StatusCreated, createIdpResponse{
IdentityProviderInfo: partial.IdentityProvider,
SetupWarning: &idpSetupWarning{
Code: oci.IdpSetupWarningCode, Message: "JIT 配置未完成,自动回滚状态无法确认,请刷新列表检查",
ResourceCreated: true, RequestID: requestID,
},
})
}
func logPartialIdpCreate(c *gin.Context, partial *oci.PartialIdentityProviderCreateError) string {
requestID := newRequestID()
detail := errors.Unwrap(partial)
if detail == nil {
detail = partial
}
log.Printf("[WARN %s] %s %s: partial IdP create: %v", requestID, c.Request.Method, requestPath(c), detail)
return requestID
}
// @Summary 激活身份提供商