发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
This commit is contained in:
@@ -18,10 +18,11 @@ type authHandler struct {
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
// 字段长度上限防超长输入撑爆登录守卫与 bcrypt(S-03)
|
||||
Username string `json:"username" binding:"required,max=64"`
|
||||
Password string `json:"password" binding:"required,max=128"`
|
||||
// 两步验证码;账号已启用 TOTP 时必填,缺失返回 428
|
||||
TotpCode string `json:"totpCode"`
|
||||
TotpCode string `json:"totpCode" binding:"max=8"`
|
||||
}
|
||||
|
||||
// login 校验用户名密码(与可选 TOTP)后签发 JWT。
|
||||
@@ -64,7 +65,7 @@ func (h *authHandler) login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
h.recordLogin(c, req.Username, http.StatusOK, start)
|
||||
|
||||
+38
-10
@@ -81,7 +81,7 @@ func (h *authxHandler) totpActivate(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
h.respondFreshToken(c)
|
||||
}
|
||||
|
||||
// totpDisable 停用两步验证;需当前验证码或登录密码任一确认。
|
||||
@@ -110,7 +110,34 @@ func (h *authxHandler) totpDisable(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
h.respondFreshToken(c)
|
||||
}
|
||||
|
||||
// respondFreshToken 敏感变更后为操作者签发新令牌返回(版本已递增,
|
||||
// 旧令牌全部失效);签发失败降级 204,前端按会话失效走重新登录。
|
||||
func (h *authxHandler) respondFreshToken(c *gin.Context) {
|
||||
token, expires, err := h.auth.IssueToken(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
c.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"token": token, "expiresAt": expires})
|
||||
}
|
||||
|
||||
// revokeSessions 撤销全部会话(令牌版本递增),并为当前操作者重签新令牌。
|
||||
//
|
||||
// @Summary 撤销全部会话
|
||||
// @Tags 认证
|
||||
// @Success 200 {object} map[string]string "新 token 与 expiresAt"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/auth/revoke-sessions [post]
|
||||
func (h *authxHandler) revokeSessions(c *gin.Context) {
|
||||
token, expires, err := h.auth.RevokeSessions(c.Request.Context(), c.GetString(usernameKey))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"token": token, "expiresAt": expires})
|
||||
}
|
||||
|
||||
// ---- 登录凭据(JWT 组内) ----
|
||||
@@ -149,7 +176,7 @@ func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.auth.UpdateCredentials(c.Request.Context(), c.GetString(usernameKey), req)
|
||||
finalName, err := h.auth.UpdateCredentials(c.Request.Context(), c.GetString(usernameKey), req)
|
||||
if errors.Is(err, service.ErrCredentialConfirm) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -162,7 +189,8 @@ func (h *authxHandler) updateCredentials(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
c.Set(usernameKey, finalName)
|
||||
h.respondFreshToken(c)
|
||||
}
|
||||
|
||||
// updatePasswordLogin 保存密码登录禁用开关;开启需至少绑定一个外部身份。
|
||||
@@ -191,7 +219,7 @@ func (h *authxHandler) updatePasswordLogin(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
h.respondFreshToken(c)
|
||||
}
|
||||
|
||||
// ---- OAuth ----
|
||||
@@ -254,7 +282,7 @@ func (h *authxHandler) bearerUser(c *gin.Context) (string, bool) {
|
||||
if len(header) < 8 || header[:7] != "Bearer " {
|
||||
return "", false
|
||||
}
|
||||
username, err := h.auth.ParseToken(header[7:])
|
||||
username, err := h.auth.ParseToken(c.Request.Context(), header[7:])
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
@@ -282,9 +310,9 @@ func (h *authxHandler) oauthCallback(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, target+"?oauthError="+url.QueryEscape(oauthErrText(err)))
|
||||
return
|
||||
}
|
||||
if token == "" {
|
||||
// 绑定模式:回设置页安全 tab
|
||||
c.Redirect(http.StatusFound, "/settings?oauth=bound")
|
||||
if mode == "bind" {
|
||||
// 绑定模式:版本已递增,新 token 经 fragment 带回设置页无感换发
|
||||
c.Redirect(http.StatusFound, "/settings?oauth=bound#oauthToken="+url.QueryEscape(token))
|
||||
return
|
||||
}
|
||||
// 登录模式:token 放 fragment,不进服务端日志与 Referer
|
||||
@@ -340,7 +368,7 @@ func (h *authxHandler) unbindIdentity(c *gin.Context) {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
h.respondFreshToken(c)
|
||||
}
|
||||
|
||||
// ---- OAuth provider 设置(JWT 组内) ----
|
||||
|
||||
@@ -40,6 +40,7 @@ func boolOr(v *bool, def bool) bool {
|
||||
// @Summary 身份提供商列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-providers [get]
|
||||
@@ -48,7 +49,7 @@ func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
idps, err := h.svc.IdentityProviders(c.Request.Context(), id)
|
||||
idps, err := h.svc.IdentityProviders(c.Request.Context(), id, c.Query("domainId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -59,6 +60,7 @@ func (h *ociConfigHandler) listIdentityProviders(c *gin.Context) {
|
||||
// @Summary 创建身份提供商(SAML)
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param body body createIdpRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -73,7 +75,7 @@ func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
idp, err := h.svc.CreateIdentityProvider(c.Request.Context(), id, oci.CreateIdpInput{
|
||||
idp, err := h.svc.CreateIdentityProvider(c.Request.Context(), id, c.Query("domainId"), oci.CreateIdpInput{
|
||||
Name: req.Name,
|
||||
Metadata: req.Metadata,
|
||||
Description: req.Description,
|
||||
@@ -97,6 +99,7 @@ func (h *ociConfigHandler) createIdentityProvider(c *gin.Context) {
|
||||
// @Summary 激活身份提供商
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param idpId path string true "idpId"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
@@ -114,7 +117,7 @@ func (h *ociConfigHandler) activateIdentityProvider(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
idp, err := h.svc.SetIdentityProviderEnabled(c.Request.Context(), id, c.Param("idpId"), *req.Enabled)
|
||||
idp, err := h.svc.SetIdentityProviderEnabled(c.Request.Context(), id, c.Query("domainId"), c.Param("idpId"), *req.Enabled)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -125,6 +128,7 @@ func (h *ociConfigHandler) activateIdentityProvider(c *gin.Context) {
|
||||
// @Summary 删除身份提供商
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param idpId path string true "idpId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
@@ -134,7 +138,7 @@ func (h *ociConfigHandler) deleteIdentityProvider(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteIdentityProvider(c.Request.Context(), id, c.Param("idpId")); err != nil {
|
||||
if err := h.svc.DeleteIdentityProvider(c.Request.Context(), id, c.Query("domainId"), c.Param("idpId")); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
@@ -144,6 +148,7 @@ func (h *ociConfigHandler) deleteIdentityProvider(c *gin.Context) {
|
||||
// @Summary 下载 SAML 元数据
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/saml-metadata [get]
|
||||
@@ -152,7 +157,7 @@ func (h *ociConfigHandler) downloadSamlMetadata(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
metadata, err := h.svc.DomainSamlMetadata(c.Request.Context(), id)
|
||||
metadata, err := h.svc.DomainSamlMetadata(c.Request.Context(), id, c.Query("domainId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -164,6 +169,7 @@ func (h *ociConfigHandler) downloadSamlMetadata(c *gin.Context) {
|
||||
// @Summary 单点登录规则列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/sign-on-rules [get]
|
||||
@@ -172,7 +178,7 @@ func (h *ociConfigHandler) listSignOnRules(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
rules, err := h.svc.ConsoleSignOnRules(c.Request.Context(), id)
|
||||
rules, err := h.svc.ConsoleSignOnRules(c.Request.Context(), id, c.Query("domainId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -183,6 +189,7 @@ func (h *ociConfigHandler) listSignOnRules(c *gin.Context) {
|
||||
// @Summary 创建 MFA 豁免规则
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -199,7 +206,7 @@ func (h *ociConfigHandler) createMfaExemption(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rule, err := h.svc.CreateMfaExemption(c.Request.Context(), id, req.IdentityProviderID)
|
||||
rule, err := h.svc.CreateMfaExemption(c.Request.Context(), id, c.Query("domainId"), req.IdentityProviderID)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -210,6 +217,7 @@ func (h *ociConfigHandler) createMfaExemption(c *gin.Context) {
|
||||
// @Summary 删除 MFA 豁免规则
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param ruleId path string true "ruleId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
@@ -219,7 +227,7 @@ func (h *ociConfigHandler) deleteMfaExemption(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteMfaExemption(c.Request.Context(), id, c.Param("ruleId")); err != nil {
|
||||
if err := h.svc.DeleteMfaExemption(c.Request.Context(), id, c.Query("domainId"), c.Param("ruleId")); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"oci-portal/internal/model"
|
||||
"oci-portal/internal/service"
|
||||
)
|
||||
|
||||
@@ -106,6 +107,127 @@ func (h *logEventHandler) list(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"items": items, "total": total})
|
||||
}
|
||||
|
||||
// alertRuleRequest 是创建/更新告警规则的请求体。
|
||||
type alertRuleRequest struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
OciConfigID uint `json:"ociConfigId"`
|
||||
EventTypes string `json:"eventTypes"`
|
||||
SourceIPs string `json:"sourceIps"`
|
||||
SourceIPMode string `json:"sourceIpMode"`
|
||||
ResourceMatch string `json:"resourceMatch"`
|
||||
Threshold int `json:"threshold"`
|
||||
WindowMinutes int `json:"windowMinutes"`
|
||||
}
|
||||
|
||||
// toModel 转为规则模型(默认阈值 1)。
|
||||
func (r alertRuleRequest) toModel() model.AlertRule {
|
||||
if r.Threshold == 0 {
|
||||
r.Threshold = 1
|
||||
}
|
||||
return model.AlertRule{
|
||||
Name: r.Name, Enabled: r.Enabled, OciConfigID: r.OciConfigID,
|
||||
EventTypes: r.EventTypes, SourceIPs: r.SourceIPs, SourceIPMode: r.SourceIPMode,
|
||||
ResourceMatch: r.ResourceMatch, Threshold: r.Threshold, WindowMinutes: r.WindowMinutes,
|
||||
}
|
||||
}
|
||||
|
||||
// listAlertRules 返回全部告警规则。
|
||||
//
|
||||
// @Summary 告警规则列表
|
||||
// @Tags 任务与日志回传
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/log-events/alert-rules [get]
|
||||
func (h *logEventHandler) listAlertRules(c *gin.Context) {
|
||||
rules, err := h.svc.ListAlertRules(c.Request.Context())
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": rules})
|
||||
}
|
||||
|
||||
// createAlertRule 创建告警规则;字段非法返回 400。
|
||||
//
|
||||
// @Summary 创建告警规则
|
||||
// @Tags 任务与日志回传
|
||||
// @Param body body alertRuleRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/log-events/alert-rules [post]
|
||||
func (h *logEventHandler) createAlertRule(c *gin.Context) {
|
||||
var req alertRuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rule, err := h.svc.CreateAlertRule(c.Request.Context(), req.toModel())
|
||||
if err != nil {
|
||||
respondAlertRuleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, rule)
|
||||
}
|
||||
|
||||
// updateAlertRule 整体覆盖告警规则(含启停)。
|
||||
//
|
||||
// @Summary 更新告警规则
|
||||
// @Tags 任务与日志回传
|
||||
// @Param ruleId path int true "规则 ID"
|
||||
// @Param body body alertRuleRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/log-events/alert-rules/{ruleId} [put]
|
||||
func (h *logEventHandler) updateAlertRule(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("ruleId"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "规则 ID 非法"})
|
||||
return
|
||||
}
|
||||
var req alertRuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rule, err := h.svc.UpdateAlertRule(c.Request.Context(), uint(id), req.toModel())
|
||||
if err != nil {
|
||||
respondAlertRuleError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, rule)
|
||||
}
|
||||
|
||||
// deleteAlertRule 删除告警规则及其命中记录。
|
||||
//
|
||||
// @Summary 删除告警规则
|
||||
// @Tags 任务与日志回传
|
||||
// @Param ruleId path int true "规则 ID"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/log-events/alert-rules/{ruleId} [delete]
|
||||
func (h *logEventHandler) deleteAlertRule(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("ruleId"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "规则 ID 非法"})
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteAlertRule(c.Request.Context(), uint(id)); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// respondAlertRuleError 把字段校验错误映射为 400。
|
||||
func respondAlertRuleError(c *gin.Context, err error) {
|
||||
if errors.Is(err, service.ErrInvalidAlertRule) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
respondError(c, err)
|
||||
}
|
||||
|
||||
// getRelay 查询 OCI 侧链路状态与关键事件清单。
|
||||
//
|
||||
// @Summary 查询 OCI 侧链路状态与关键事件清单
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
// usernameKey 是鉴权通过后写入 gin.Context 的用户名键。
|
||||
const usernameKey = "username"
|
||||
|
||||
// RequireAuth 校验 Authorization: Bearer 令牌,通过后把用户名放进上下文。
|
||||
// RequireAuth 校验 Authorization: Bearer 令牌(签名、有效期与令牌版本),
|
||||
// 通过后把用户名放进上下文。
|
||||
func RequireAuth(auth *service.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token, ok := strings.CutPrefix(c.GetHeader("Authorization"), "Bearer ")
|
||||
@@ -23,7 +24,7 @@ func RequireAuth(auth *service.AuthService) gin.HandlerFunc {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"})
|
||||
return
|
||||
}
|
||||
username, err := auth.ParseToken(token)
|
||||
username, err := auth.ParseToken(c.Request.Context(), token)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
|
||||
return
|
||||
@@ -33,6 +34,17 @@ func RequireAuth(auth *service.AuthService) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// bodyLimit 给请求体套上限(S-03):超限读取由 MaxBytesReader 截断报错,
|
||||
// 绑定失败返回 400;webhook 入口另有独立 256KiB 自限,不经此中间件。
|
||||
func bodyLimit(n int64) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Body != nil {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n)
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// systemLogMiddleware 把写请求(POST/PUT/DELETE)异步记入系统日志,只读请求跳过。
|
||||
// 只记方法、路径等元数据,绝不读请求体——请求体可能含私钥 / 口令等敏感数据。
|
||||
func systemLogMiddleware(logs *service.SystemLogService) gin.HandlerFunc {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -74,7 +77,7 @@ func (h *ociConfigHandler) create(c *gin.Context) {
|
||||
func (h *ociConfigHandler) list(c *gin.Context) {
|
||||
items, err := h.svc.List(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
@@ -168,6 +171,7 @@ func (h *ociConfigHandler) update(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Summary 删除租户配置
|
||||
// @Description 删除租户配置及其本地关联任务、快照、回传、告警和 AI 渠道数据,并撤销 Webhook 密钥;不删除 OCI 云端资源。
|
||||
// @Tags 租户配置
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 204 "无内容"
|
||||
@@ -258,17 +262,29 @@ func pathID(c *gin.Context) (uint, bool) {
|
||||
return uint(id), true
|
||||
}
|
||||
|
||||
// respondError 统一出错响应边界(S-08):OCI 服务错误经脱敏透出,记录缺失
|
||||
// 映射 404;其余按内部错误处理——响应只含固定文案与关联 ID,完整原因
|
||||
// (可能携带 SQL / DSN / 路径等内部细节)仅写服务端日志,经同一 ID 对应。
|
||||
func respondError(c *gin.Context, err error) {
|
||||
status := http.StatusInternalServerError
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
var svcErr common.ServiceError
|
||||
if errors.As(err, &svcErr) {
|
||||
respondOCIError(c, err, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
|
||||
return
|
||||
}
|
||||
id := newRequestID()
|
||||
log.Printf("[ERR %s] %s %s: %v", id, c.Request.Method, requestPath(c), err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器内部错误", "requestId": id})
|
||||
}
|
||||
|
||||
// newRequestID 生成错误关联 ID(8 字节随机 hex),响应与服务端日志据此对应。
|
||||
func newRequestID() string {
|
||||
b := make([]byte, 8)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// respondOCIError 提炼 OCI 服务错误:透传 HTTP 状态码,
|
||||
@@ -278,9 +294,10 @@ func respondOCIError(c *gin.Context, err error, svcErr common.ServiceError) {
|
||||
if status < http.StatusBadRequest {
|
||||
status = http.StatusBadGateway
|
||||
}
|
||||
// 面板的 401 专属本地 JWT 失效(前端收到即登出);
|
||||
// 上游 OCI 的 401 是代理调用被拒,改用 502 透出
|
||||
if status == http.StatusUnauthorized {
|
||||
// 面板的 401 专属本地 JWT 失效(前端收到即登出)、429 专属本地 IP 限速
|
||||
// (前端收到即跳 /blocked);上游 OCI 的同码是代理调用被拒/云端限流,
|
||||
// 均改用 502 透出,body 保留 ociCode 供辨识
|
||||
if status == http.StatusUnauthorized || status == http.StatusTooManyRequests {
|
||||
status = http.StatusBadGateway
|
||||
}
|
||||
body := gin.H{
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// fakeServiceError 模拟 oci-go-sdk 的 common.ServiceError,驱动 respondOCIError 分支。
|
||||
type fakeServiceError struct {
|
||||
status int
|
||||
code string
|
||||
message string
|
||||
}
|
||||
|
||||
func (e fakeServiceError) Error() string {
|
||||
return fmt.Sprintf("Error returned by service. Http Status Code: %d. Error Code: %s. Message: %s",
|
||||
e.status, e.code, e.message)
|
||||
}
|
||||
|
||||
func (e fakeServiceError) GetHTTPStatusCode() int { return e.status }
|
||||
func (e fakeServiceError) GetMessage() string { return e.message }
|
||||
func (e fakeServiceError) GetCode() string { return e.code }
|
||||
func (e fakeServiceError) GetOpcRequestID() string { return "req-1" }
|
||||
|
||||
// TestRespondErrorOCIStatus 覆盖上游状态码改写规则:
|
||||
// 401/429 在前端有本地专属语义(登出 / 跳 /blocked),上游同码须改写 502。
|
||||
func TestRespondErrorOCIStatus(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
cases := []struct {
|
||||
name string
|
||||
upstream int
|
||||
wantStatus int
|
||||
}{
|
||||
{"404 原样透传", 404, 404},
|
||||
{"409 原样透传", 409, 409},
|
||||
{"上游 401 改写 502", 401, 502},
|
||||
{"上游 429 改写 502", 429, 502},
|
||||
{"非错误码兜底 502", 200, 502},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
err := fmt.Errorf("获取流量: %w", fakeServiceError{tc.upstream, "TooManyRequests", "slow down"})
|
||||
respondError(c, err)
|
||||
if rec.Code != tc.wantStatus {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, tc.wantStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
func listRegions(c *gin.Context) {
|
||||
regions, err := oci.AllRegions()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, regions)
|
||||
|
||||
@@ -21,7 +21,8 @@ func NewRouter(auth *service.AuthService, oauth *service.OAuthService, ociConfig
|
||||
accessLog := gin.LoggerWithConfig(gin.LoggerConfig{SkipQueryString: true})
|
||||
r.Use(accessLog, RealIPMiddleware(settings), IPRateMiddleware(settings), gin.Recovery())
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
// 面板 API 请求体统一 1MB 上限(S-03);AI 网关按对话体量单独 10MB
|
||||
v1 := r.Group("/api/v1", bodyLimit(1<<20))
|
||||
registerAuthPublic(v1, auth, oauth, systemLogs)
|
||||
// AI 网关对外端点:独立密钥鉴权,挂在全局 Use 之后,仍受 IP 限速与 Recovery 保护
|
||||
registerAiGateway(r, aiGateway)
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -183,3 +185,103 @@ func TestSystemLogsEndpoint(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoginFieldLimits 超长登录字段被绑定校验拒绝(S-03 高基数防护第一道)。
|
||||
func TestLoginFieldLimits(t *testing.T) {
|
||||
r, _, _ := newTestRouter(t)
|
||||
longName := strings.Repeat("a", 65)
|
||||
longPass := strings.Repeat("b", 129)
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{name: "用户名超长", body: `{"username":"` + longName + `","password":"x"}`},
|
||||
{name: "密码超长", body: `{"username":"admin","password":"` + longPass + `"}`},
|
||||
{name: "验证码超长", body: `{"username":"admin","password":"x","totpCode":"123456789"}`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", tt.body)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want 400", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBodyLimitRejectsHuge 面板 API 请求体超 1MB 被拒(S-03)。
|
||||
func TestBodyLimitRejectsHuge(t *testing.T) {
|
||||
r, _, _ := newTestRouter(t)
|
||||
huge := `{"username":"admin","password":"` + strings.Repeat("x", 2<<20) + `"}`
|
||||
w := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", huge)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want 400(body too large)", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRevokeSessionsEndpoint 撤销全部会话:直接调用即生效,
|
||||
// 响应带新 token,旧 token 随即失效。
|
||||
func TestRevokeSessionsEndpoint(t *testing.T) {
|
||||
r, _, _ := newTestRouter(t)
|
||||
login := doRequest(t, r, http.MethodPost, "/api/v1/auth/login", "", `{"username":"admin","password":"pass123"}`)
|
||||
var sess struct{ Token string }
|
||||
if err := json.Unmarshal(login.Body.Bytes(), &sess); err != nil || sess.Token == "" {
|
||||
t.Fatalf("login: %s", login.Body.String())
|
||||
}
|
||||
w := doRequest(t, r, http.MethodPost, "/api/v1/auth/revoke-sessions", sess.Token, "")
|
||||
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "token") {
|
||||
t.Fatalf("revoke = %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
w = doRequest(t, r, http.MethodGet, "/api/v1/auth/credentials", sess.Token, "")
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("撤销后旧 token 访问 = %d, want 401", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRespondErrorSanitizesInternal 内部错误边界(S-08):wrap 链细节不透传,
|
||||
// 响应为固定文案 + requestId;记录缺失映射 404 固定文案。
|
||||
func TestRespondErrorSanitizesInternal(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
cases := []struct {
|
||||
name string
|
||||
err error
|
||||
wantStatus int
|
||||
wantBody string // 必须出现
|
||||
leaks []string // 不得出现
|
||||
}{
|
||||
{
|
||||
name: "内部错误脱敏",
|
||||
err: fmt.Errorf("query users: dial tcp 10.0.0.5:3306: dsn user:secretpw"),
|
||||
wantStatus: http.StatusInternalServerError,
|
||||
wantBody: "requestId",
|
||||
leaks: []string{"secretpw", "10.0.0.5", "dsn", "dial tcp"},
|
||||
},
|
||||
{
|
||||
name: "记录缺失固定文案",
|
||||
err: fmt.Errorf("find oci config 5: %w", gorm.ErrRecordNotFound),
|
||||
wantStatus: http.StatusNotFound,
|
||||
wantBody: "资源不存在",
|
||||
leaks: []string{"find oci config"},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/x", nil)
|
||||
respondError(c, tc.err)
|
||||
if rec.Code != tc.wantStatus {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, tc.wantStatus)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, tc.wantBody) {
|
||||
t.Errorf("body %q 应包含 %q", body, tc.wantBody)
|
||||
}
|
||||
for _, leak := range tc.leaks {
|
||||
if strings.Contains(body, leak) {
|
||||
t.Errorf("body %q 泄露内部细节 %q", body, leak)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// 挂在全局中间件之后,仍受 IP 限速与 Recovery 保护;高频调用自有 AiCallLog。
|
||||
func registerAiGateway(r *gin.Engine, aiGateway *service.AiGatewayService) {
|
||||
aih := &aiGatewayHandler{gw: aiGateway}
|
||||
ai := r.Group("/ai/v1", aih.auth)
|
||||
ai := r.Group("/ai/v1", bodyLimit(10<<20), aih.auth)
|
||||
ai.POST("/chat/completions", aih.chatCompletions)
|
||||
ai.POST("/responses", aih.responses)
|
||||
ai.POST("/messages", aih.messages)
|
||||
|
||||
@@ -26,13 +26,14 @@ func registerAuthSecured(secured *gin.RouterGroup, auth *service.AuthService, oa
|
||||
ax := &authxHandler{auth: auth, oauth: oauth}
|
||||
secured.GET("/auth/totp", ax.totpStatus)
|
||||
secured.POST("/auth/totp/setup", ax.totpSetup)
|
||||
secured.POST("/auth/totp/activate", ax.totpActivate)
|
||||
secured.POST("/auth/totp/disable", ax.totpDisable)
|
||||
secured.GET("/auth/credentials", ax.getCredentials)
|
||||
secured.PUT("/auth/credentials", ax.updateCredentials)
|
||||
secured.PUT("/auth/password-login", ax.updatePasswordLogin)
|
||||
secured.GET("/auth/identities", ax.identities)
|
||||
secured.POST("/auth/totp/activate", ax.totpActivate)
|
||||
secured.POST("/auth/totp/disable", ax.totpDisable)
|
||||
secured.PUT("/auth/password-login", ax.updatePasswordLogin)
|
||||
secured.DELETE("/auth/identities/:id", ax.unbindIdentity)
|
||||
secured.POST("/auth/revoke-sessions", ax.revokeSessions)
|
||||
secured.GET("/settings/oauth", func(c *gin.Context) { ax.getOAuthSettings(c, settings) })
|
||||
secured.PUT("/settings/oauth", func(c *gin.Context) { ax.updateOAuthSettings(c, settings) })
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ func registerOciCost(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
func registerOciTenantIAM(g *gin.RouterGroup, h *ociConfigHandler) {
|
||||
g.GET("/oci-configs/:id/audit-events", h.getAuditEvents)
|
||||
g.GET("/oci-configs/:id/audit-events/detail", h.getAuditEventDetail)
|
||||
g.GET("/oci-configs/:id/domains", h.listIdentityDomains)
|
||||
g.GET("/oci-configs/:id/users", h.listTenantUsers)
|
||||
g.POST("/oci-configs/:id/users", h.createTenantUser)
|
||||
g.GET("/oci-configs/:id/users/:userId", h.getTenantUserDetail)
|
||||
|
||||
@@ -15,6 +15,9 @@ func registerSettings(secured *gin.RouterGroup, settings *service.SettingService
|
||||
secured.GET("/settings/telegram", st.getTelegram)
|
||||
secured.PUT("/settings/telegram", st.updateTelegram)
|
||||
secured.POST("/settings/telegram/test", st.testTelegram)
|
||||
secured.GET("/settings/notify-channels", st.listNotifyChannels)
|
||||
secured.PUT("/settings/notify-channels/:type", st.updateNotifyChannel)
|
||||
secured.POST("/settings/notify-channels/:type/test", st.testNotifyChannel)
|
||||
secured.GET("/settings/notify-events", st.getNotifyEvents)
|
||||
secured.PUT("/settings/notify-events", st.updateNotifyEvents)
|
||||
secured.GET("/settings/notify-templates", st.listNotifyTemplates)
|
||||
|
||||
@@ -19,6 +19,10 @@ func registerTasksAndLogs(secured *gin.RouterGroup, tasks *service.TaskService,
|
||||
|
||||
le := &logEventHandler{svc: logEvents}
|
||||
secured.GET("/log-events", le.list)
|
||||
secured.GET("/log-events/alert-rules", le.listAlertRules)
|
||||
secured.POST("/log-events/alert-rules", le.createAlertRule)
|
||||
secured.PUT("/log-events/alert-rules/:ruleId", le.updateAlertRule)
|
||||
secured.DELETE("/log-events/alert-rules/:ruleId", le.deleteAlertRule)
|
||||
secured.GET("/oci-configs/:id/log-webhook", le.getWebhook)
|
||||
secured.POST("/oci-configs/:id/log-webhook", le.ensureWebhook)
|
||||
secured.DELETE("/oci-configs/:id/log-webhook", le.revokeWebhook)
|
||||
|
||||
@@ -80,6 +80,82 @@ func (h *settingsHandler) testTelegram(c *gin.Context) {
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// listNotifyChannels 返回全部通知渠道的脱敏视图(webhook/ntfy/bark/smtp,不含 telegram)。
|
||||
//
|
||||
// @Summary 返回全部通知渠道的脱敏视图
|
||||
// @Tags 设置
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-channels [get]
|
||||
func (h *settingsHandler) listNotifyChannels(c *gin.Context) {
|
||||
items, err := h.svc.NotifyChannels(c.Request.Context())
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
// updateNotifyChannelRequest 是保存单渠道配置的请求体;
|
||||
// secret 承载渠道密文字段(ntfy token / bark device key / smtp 密码),
|
||||
// 缺省沿用已存值,空串清除。
|
||||
type updateNotifyChannelRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
URL string `json:"url"`
|
||||
BodyTemplate string `json:"bodyTemplate"`
|
||||
Server string `json:"server"`
|
||||
Topic string `json:"topic"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Secret *string `json:"secret"`
|
||||
}
|
||||
|
||||
// updateNotifyChannel 保存单渠道配置并返回全渠道最新视图;未知类型或校验失败返回 400。
|
||||
//
|
||||
// @Summary 保存单渠道配置并返回全渠道最新视图
|
||||
// @Tags 设置
|
||||
// @Param type path string true "渠道类型(webhook/ntfy/bark/smtp)"
|
||||
// @Param body body updateNotifyChannelRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-channels/{type} [put]
|
||||
func (h *settingsHandler) updateNotifyChannel(c *gin.Context) {
|
||||
var req updateNotifyChannelRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err := h.svc.UpdateNotifyChannel(c.Request.Context(), c.Param("type"), service.UpdateNotifyChannelInput{
|
||||
Enabled: req.Enabled, URL: req.URL, BodyTemplate: req.BodyTemplate,
|
||||
Server: req.Server, Topic: req.Topic, Host: req.Host, Port: req.Port,
|
||||
Username: req.Username, From: req.From, To: req.To, Secret: req.Secret,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
h.listNotifyChannels(c)
|
||||
}
|
||||
|
||||
// testNotifyChannel 用已保存配置向指定渠道同步发送测试消息。
|
||||
//
|
||||
// @Summary 向指定渠道发送测试消息
|
||||
// @Tags 设置
|
||||
// @Param type path string true "渠道类型(webhook/ntfy/bark/smtp)"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/settings/notify-channels/{type}/test [post]
|
||||
func (h *settingsHandler) testNotifyChannel(c *gin.Context) {
|
||||
if err := h.notifier.SendChannelTest(c.Request.Context(), c.Param("type")); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// getNotifyEvents 返回全部通知事件开关;键缺省(存量部署)视为开启。
|
||||
//
|
||||
// @Summary 返回全部通知事件开关
|
||||
|
||||
+53
-22
@@ -67,12 +67,14 @@ func (h *ociConfigHandler) costs(c *gin.Context) {
|
||||
|
||||
// ---- 租户审计日志 ----
|
||||
|
||||
// getAuditEvents 实时查询租户 OCI 审计事件:hours 首查(缺省 24);
|
||||
// start/end/page 为截断后的同窗续查;窗口越界或非法返回 400。
|
||||
// getAuditEvents 批式懒加载查询审计事件:cursor 为空自当前时刻首查,
|
||||
// 非空从上次响应游标继续向更早回溯;limit 单批目标条数(缺省 100,上限 200)。
|
||||
//
|
||||
// @Summary 实时查询租户 OCI 审计事件:hours 首查
|
||||
// @Summary 批式懒加载查询租户 OCI 审计事件
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param cursor query string false "续查游标(上次响应原样带回)"
|
||||
// @Param limit query int false "单批目标条数,缺省 100,上限 200"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/audit-events [get]
|
||||
@@ -81,13 +83,10 @@ func (h *ociConfigHandler) getAuditEvents(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
hours, _ := strconv.Atoi(c.DefaultQuery("hours", "24"))
|
||||
q := service.AuditQuery{
|
||||
Region: c.Query("region"), Hours: hours,
|
||||
Start: c.Query("start"), End: c.Query("end"), Page: c.Query("page"),
|
||||
}
|
||||
limit, _ := strconv.Atoi(c.Query("limit"))
|
||||
q := service.AuditQuery{Region: c.Query("region"), Cursor: c.Query("cursor"), Limit: limit}
|
||||
result, err := h.svc.AuditEvents(c.Request.Context(), id, q)
|
||||
if errors.Is(err, service.ErrInvalidAuditHours) || errors.Is(err, service.ErrInvalidAuditWindow) {
|
||||
if errors.Is(err, service.ErrInvalidAuditCursor) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -143,9 +142,29 @@ type createTenantUserRequest struct {
|
||||
AddToAdminGroup bool `json:"addToAdminGroup"`
|
||||
}
|
||||
|
||||
// @Summary 租户身份域列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/domains [get]
|
||||
func (h *ociConfigHandler) listIdentityDomains(c *gin.Context) {
|
||||
id, ok := pathID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
domains, err := h.svc.IdentityDomains(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, domains)
|
||||
}
|
||||
|
||||
// @Summary 租户 IAM 用户列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/users [get]
|
||||
@@ -154,7 +173,7 @@ func (h *ociConfigHandler) listTenantUsers(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
users, err := h.svc.TenantUsers(c.Request.Context(), id)
|
||||
users, err := h.svc.TenantUsers(c.Request.Context(), id, c.Query("domainId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -165,6 +184,7 @@ func (h *ociConfigHandler) listTenantUsers(c *gin.Context) {
|
||||
// @Summary 租户用户详情
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -174,7 +194,7 @@ func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
detail, err := h.svc.TenantUserDetail(c.Request.Context(), id, c.Param("userId"))
|
||||
detail, err := h.svc.TenantUserDetail(c.Request.Context(), id, c.Query("domainId"), c.Param("userId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -185,6 +205,7 @@ func (h *ociConfigHandler) getTenantUserDetail(c *gin.Context) {
|
||||
// @Summary 创建租户 IAM 用户
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param body body createTenantUserRequest true "请求体"
|
||||
// @Success 201 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -199,7 +220,7 @@ func (h *ociConfigHandler) createTenantUser(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, err := h.svc.CreateTenantUser(c.Request.Context(), id, oci.CreateTenantUserInput{
|
||||
user, err := h.svc.CreateTenantUser(c.Request.Context(), id, c.Query("domainId"), oci.CreateTenantUserInput{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Email: req.Email,
|
||||
@@ -229,6 +250,7 @@ type updateTenantUserRequest struct {
|
||||
// @Summary 更新租户 IAM 用户
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param userId path string true "userId"
|
||||
// @Param body body updateTenantUserRequest true "请求体"
|
||||
// @Success 200 {object} map[string]any
|
||||
@@ -244,7 +266,7 @@ func (h *ociConfigHandler) updateTenantUser(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, err := h.svc.UpdateTenantUser(c.Request.Context(), id, c.Param("userId"), oci.UpdateTenantUserInput{
|
||||
user, err := h.svc.UpdateTenantUser(c.Request.Context(), id, c.Query("domainId"), c.Param("userId"), oci.UpdateTenantUserInput{
|
||||
Description: req.Description,
|
||||
Email: req.Email,
|
||||
GivenName: req.GivenName,
|
||||
@@ -262,6 +284,7 @@ func (h *ociConfigHandler) updateTenantUser(c *gin.Context) {
|
||||
// @Summary 删除租户 IAM 用户
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 204 "无内容"
|
||||
// @Security BearerAuth
|
||||
@@ -271,7 +294,7 @@ func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteTenantUser(c.Request.Context(), id, c.Param("userId")); err != nil {
|
||||
if err := h.svc.DeleteTenantUser(c.Request.Context(), id, c.Query("domainId"), c.Param("userId")); err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
}
|
||||
@@ -281,6 +304,7 @@ func (h *ociConfigHandler) deleteTenantUser(c *gin.Context) {
|
||||
// @Summary 重置租户用户密码
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -290,7 +314,7 @@ func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
password, err := h.svc.ResetTenantUserPassword(c.Request.Context(), id, c.Param("userId"))
|
||||
password, err := h.svc.ResetTenantUserPassword(c.Request.Context(), id, c.Query("domainId"), c.Param("userId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -301,6 +325,7 @@ func (h *ociConfigHandler) resetTenantUserPassword(c *gin.Context) {
|
||||
// @Summary 清除用户 MFA 设备
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param userId path string true "userId"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -310,7 +335,7 @@ func (h *ociConfigHandler) deleteTenantUserMfa(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
deleted, err := h.svc.DeleteTenantUserMfa(c.Request.Context(), id, c.Param("userId"))
|
||||
deleted, err := h.svc.DeleteTenantUserMfa(c.Request.Context(), id, c.Query("domainId"), c.Param("userId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -344,6 +369,7 @@ func (h *ociConfigHandler) deleteTenantUserApiKeys(c *gin.Context) {
|
||||
// @Summary ---- 域通知收件人与密码策略 ----
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/notification-recipients [get]
|
||||
@@ -352,7 +378,7 @@ func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
recipients, err := h.svc.NotificationRecipients(c.Request.Context(), id)
|
||||
recipients, err := h.svc.NotificationRecipients(c.Request.Context(), id, c.Query("domainId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -363,6 +389,7 @@ func (h *ociConfigHandler) getNotificationRecipients(c *gin.Context) {
|
||||
// @Summary 更新通知收件人
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -379,7 +406,7 @@ func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
recipients, err := h.svc.UpdateNotificationRecipients(c.Request.Context(), id, req.Recipients)
|
||||
recipients, err := h.svc.UpdateNotificationRecipients(c.Request.Context(), id, c.Query("domainId"), req.Recipients)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -390,6 +417,7 @@ func (h *ociConfigHandler) updateNotificationRecipients(c *gin.Context) {
|
||||
// @Summary 密码策略列表
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/password-policies [get]
|
||||
@@ -398,7 +426,7 @@ func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
policies, err := h.svc.PasswordPolicies(c.Request.Context(), id)
|
||||
policies, err := h.svc.PasswordPolicies(c.Request.Context(), id, c.Query("domainId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -409,6 +437,7 @@ func (h *ociConfigHandler) listPasswordPolicies(c *gin.Context) {
|
||||
// @Summary 更新密码策略
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param policyId path string true "policyId"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
@@ -428,7 +457,7 @@ func (h *ociConfigHandler) updatePasswordPolicy(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
policy, err := h.svc.UpdatePasswordPolicy(c.Request.Context(), id, c.Param("policyId"), oci.UpdatePasswordPolicyInput{
|
||||
policy, err := h.svc.UpdatePasswordPolicy(c.Request.Context(), id, c.Query("domainId"), c.Param("policyId"), oci.UpdatePasswordPolicyInput{
|
||||
PasswordExpiresAfter: req.PasswordExpiresAfter,
|
||||
MinLength: req.MinLength,
|
||||
NumPasswordsInHistory: req.NumPasswordsInHistory,
|
||||
@@ -443,6 +472,7 @@ func (h *ociConfigHandler) updatePasswordPolicy(c *gin.Context) {
|
||||
// @Summary 身份域设置
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
// @Router /api/v1/oci-configs/{id}/identity-settings [get]
|
||||
@@ -451,7 +481,7 @@ func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
setting, err := h.svc.IdentitySetting(c.Request.Context(), id)
|
||||
setting, err := h.svc.IdentitySetting(c.Request.Context(), id, c.Query("domainId"))
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
@@ -462,6 +492,7 @@ func (h *ociConfigHandler) getIdentitySetting(c *gin.Context) {
|
||||
// @Summary 更新身份域设置
|
||||
// @Tags 租户 IAM
|
||||
// @Param id path int true "配置 ID"
|
||||
// @Param domainId query string false "身份域 OCID(缺省 Default 域)"
|
||||
// @Param body body object true "请求体(见接口说明)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Security BearerAuth
|
||||
@@ -478,7 +509,7 @@ func (h *ociConfigHandler) updateIdentitySetting(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
setting, err := h.svc.UpdateIdentitySetting(c.Request.Context(), id, *req.PrimaryEmailRequired)
|
||||
setting, err := h.svc.UpdateIdentitySetting(c.Request.Context(), id, c.Query("domainId"), *req.PrimaryEmailRequired)
|
||||
if err != nil {
|
||||
respondError(c, err)
|
||||
return
|
||||
|
||||
@@ -79,7 +79,7 @@ var wsUpgrader = websocket.Upgrader{
|
||||
// @Success 101 "升级为 WebSocket"
|
||||
// @Router /api/v1/console-sessions/{sessionId}/ws [get]
|
||||
func (h *consoleHandler) ws(c *gin.Context) {
|
||||
if _, err := h.auth.ParseToken(c.Query("token")); err != nil {
|
||||
if _, err := h.auth.ParseToken(c.Request.Context(), c.Query("token")); err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user