+138
-3
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Federation:SAML 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 激活身份提供商
|
||||
|
||||
Reference in New Issue
Block a user