feat(gateway): 租户自助管理接口 —— 薄 Web 面增量1
SaaS P3 收口第一刀。成员管理此前只有平台超管口径(/admin/tenants/:id/*), 租户 owner 无法自助管自己的组织;建租户同样只有 admin 能做。 - 成员自助四路由(作用于活跃租户,id 取 ctx 不接受路径任意租户): GET /tenants/current/members(≥viewer) + POST/PUT/DELETE(≥admin+审计)。 RequireTenantRole 是 RBAC 极简版预留的架子,这次直接挂上,零改动生效。 - handler 挡两个平台超管可以、租户 admin 不该有的动作:邀请为 owner; 借 AddMemberByEmail 的 upsert 语义「重新邀请」owner 实现降权——后者 是真实的越权路径,admin 拿 owner 邮箱重邀成 viewer 就把 owner 拉下马。 - POST /me/tenants 自助建组织:创建者即 owner、建完切入;slug 用雪花 id 生成不让用户填(全局唯一,自助场景撞名只添乱);owner 没挂上按错误返回 而不是 warn+200(孤儿租户没人能管)。 - CORS 支持逗号分隔多 origin(desktop 预览/薄 Web 面/admin 不同源):命中 白名单回显请求 Origin+Vary;单值/通配/生产未配置全拒的旧语义不变。 - 测试:RequireTenantRole 补 ≥admin 门槛用例;cors 四场景单测。 live 验证 13 项全过(门槛/owner 保护/降权攻击挡下/CORS 回显/审计落痕)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,14 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
p.POST("/tasks/:id/approve", middleware.Audit(db), h.ApproveTask) // HITL 人工审批决定(批准/拒绝,审计)
|
||||
p.GET("/tenants/current", h.TenantCurrent) // 当前租户上下文 + 角色 + 可花余额(多租户)
|
||||
p.GET("/me/tenants", h.MyTenantsList) // 我所属租户(供切换)
|
||||
p.POST("/me/tenants", h.CreateMyTenant) // 自助建组织(创建者即 owner,建完切入)
|
||||
p.POST("/me/tenant", h.SwitchTenant) // 切换当前活跃租户
|
||||
|
||||
// 租户成员自助管理(薄 Web 面):作用于活跃租户,看 ≥viewer、写 ≥admin + 审计。
|
||||
p.GET("/tenants/current/members", middleware.RequireTenantRole(db, store.RoleViewer), h.TenantMembers)
|
||||
p.POST("/tenants/current/members", middleware.RequireTenantRole(db, store.RoleAdmin), middleware.Audit(db), h.TenantAddMember)
|
||||
p.PUT("/tenants/current/members/:uid", middleware.RequireTenantRole(db, store.RoleAdmin), middleware.Audit(db), h.TenantSetMemberRole)
|
||||
p.DELETE("/tenants/current/members/:uid", middleware.RequireTenantRole(db, store.RoleAdmin), middleware.Audit(db), h.TenantRemoveMember)
|
||||
p.GET("/me/usage", h.MyUsage) // 我的用量明细(余额 + 趋势 + 最近消耗)
|
||||
p.GET("/tasks/:id/eval", h.TaskEval) // 自动化评测结果(综合/质量/忠实度/分级)
|
||||
p.PUT("/memory", h.SetMemory) // 偏好记忆登记(→ mcp-go memory_upsert)
|
||||
@@ -135,24 +142,39 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
return r
|
||||
}
|
||||
|
||||
// cors 控制跨源访问。允许来源经 CORS_ALLOW_ORIGIN 配置。
|
||||
// cors 控制跨源访问。允许来源经 CORS_ALLOW_ORIGIN 配置,支持逗号分隔多值
|
||||
// (桌面端 Web 预览 + 薄 Web 面 + admin 可能是不同 origin):命中则回显请求 Origin。
|
||||
// 安全默认:开发期缺省放行 "*"(便利);生产期(APP_ENV=prod / GIN_MODE=release)未显式配置则
|
||||
// **不放行任意源**(不发 ACAO 头 → 浏览器按同源策略拦截),逼运维显式配置允许的源。
|
||||
func cors() gin.HandlerFunc {
|
||||
origin := os.Getenv("CORS_ALLOW_ORIGIN")
|
||||
if origin == "" {
|
||||
raw := os.Getenv("CORS_ALLOW_ORIGIN")
|
||||
if raw == "" {
|
||||
if isProd() {
|
||||
log.Printf("[cors] 生产环境未设 CORS_ALLOW_ORIGIN:默认不放行跨源,请显式配置允许的源(如 https://app.example.com)")
|
||||
log.Printf("[cors] 生产环境未设 CORS_ALLOW_ORIGIN:默认不放行跨源,请显式配置允许的源(逗号分隔多个,如 https://app.example.com,https://console.example.com)")
|
||||
} else {
|
||||
origin = "*" // 开发便利
|
||||
raw = "*" // 开发便利
|
||||
}
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
if o = strings.TrimSpace(o); o != "" {
|
||||
allowed[o] = true
|
||||
}
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
if origin != "" {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
if origin != "*" {
|
||||
c.Header("Vary", "Origin")
|
||||
switch {
|
||||
case allowed["*"]:
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
case allowed[c.GetHeader("Origin")]:
|
||||
// 多 origin 场景 ACAO 只能发单值:命中白名单则回显请求方,并以 Vary 防缓存串源。
|
||||
c.Header("Access-Control-Allow-Origin", c.GetHeader("Origin"))
|
||||
c.Header("Vary", "Origin")
|
||||
case len(allowed) == 1:
|
||||
// 单值配置保持旧行为:无条件直写(兼容不带 Origin 头的探活/工具请求也能看到头)。
|
||||
for o := range allowed {
|
||||
c.Header("Access-Control-Allow-Origin", o)
|
||||
}
|
||||
c.Header("Vary", "Origin")
|
||||
}
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Session-ID, X-User-ID")
|
||||
|
||||
Reference in New Issue
Block a user