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:
@@ -61,3 +61,28 @@ func TestRequireTenantRole_Gate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 成员自管写路由用 ≥admin 门槛(薄 Web 面):member 也只能看不能动。
|
||||
func TestRequireTenantRole_AdminGate(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
memberRole string
|
||||
wantCode int
|
||||
}{
|
||||
{"owner 放行", store.RoleOwner, http.StatusOK},
|
||||
{"admin 放行", store.RoleAdmin, http.StatusOK},
|
||||
{"member 拦下(成员管理要 admin)", store.RoleMember, http.StatusForbidden},
|
||||
{"viewer 拦下", store.RoleViewer, http.StatusForbidden},
|
||||
{"billing_admin 拦下", store.RoleBillingAdmin, http.StatusForbidden},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := roleGateEngine("u1", "t1", tc.memberRole, store.RoleAdmin)
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/tasks", nil))
|
||||
if w.Code != tc.wantCode {
|
||||
t.Errorf("状态码=%d, 期望 %d(body=%s)", w.Code, tc.wantCode, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user