feat(server): add Wayne subsystem authorization APIs

This commit is contained in:
mac
2026-07-17 19:26:40 +08:00
parent add4b1566e
commit 8d49230247
7 changed files with 679 additions and 68 deletions
+58 -6
View File
@@ -285,14 +285,14 @@ Wayne 会把回调地址拼成:
## Wayne 授权代理接口 ## Wayne 授权代理接口
AuthServer 的 Wayne 授权代理接口要求调用方传目标 Wayne `userId`。后端会从当前 `authserver_token` 里取 `email` 作为操作者 `operatorName`,目标用户 ID 由请求体或路径参数提供。 AuthServer 的 Wayne 授权代理接口要求调用方传目标 Wayne `username`。后端会从当前 `authserver_token` 里取 `email` 作为操作者 `operatorName`,目标用户名由请求体或路径参数提供。
对外接口: 对外接口:
```text ```text
GET /auth/api/v1/wayne/namespaces GET /auth/api/v1/wayne/namespaces
GET /auth/api/v1/wayne/groups GET /auth/api/v1/wayne/groups
GET /auth/api/v1/wayne/users/:userid/roles GET /auth/api/v1/wayne/users/:username/roles
GET /auth/api/v1/wayne/namespaces/:namespaceid/operator-permissions GET /auth/api/v1/wayne/namespaces/:namespaceid/operator-permissions
GET /auth/api/v1/wayne/apps/:appid/operator-permissions GET /auth/api/v1/wayne/apps/:appid/operator-permissions
PUT /auth/api/v1/wayne/namespaces/:namespaceid/roles PUT /auth/api/v1/wayne/namespaces/:namespaceid/roles
@@ -309,7 +309,7 @@ Authorization: Bearer <authserver_token>
Content-Type: application/json Content-Type: application/json
{ {
"userId": 2001, "username": "target@example.com",
"groupIds": [10, 11], "groupIds": [10, 11],
"replace": false, "replace": false,
"requestId": "req-001", "requestId": "req-001",
@@ -317,13 +317,13 @@ Content-Type: application/json
} }
``` ```
AuthServer 转发到 Wayne internal API 时会使用请求体里的 `userId`: AuthServer 转发到 Wayne internal API 时会使用请求体里的 `username`:
```text ```text
PUT /api/v1/internal/namespaces/1/users/2001/roles PUT /api/v1/internal/namespaces/1/users/target@example.com/roles
``` ```
并覆盖请求体中的 `operatorName` 为 token email,忽略外部传入的 `operatorUserId`。`userId` 只用于 Wayne path,不会透传到 Wayne 请求体。 并覆盖请求体中的 `operatorName` 为 token email,忽略外部传入的 `operatorUserId`。`username` 只用于 Wayne path,不会透传到 Wayne 请求体。
相关配置: 相关配置:
@@ -342,6 +342,58 @@ signature = HMAC_SHA256_HEX(secret, payload)
X-Wayne-Signature = "sha256=" + signature X-Wayne-Signature = "sha256=" + signature
``` ```
## 子系统赋权接口
子系统赋权接口是平台业务层接口,前端应优先调用这一组,而不是直接调用低层 `/wayne/*` 代理。当前只实现 Wayne,CloudDM 先返回未启用占位。
权限规则:
- 平台管理员可以操作任意业务线。
- 非平台管理员必须是当前业务线管理员,也就是 `business_line_users.permission = 0`。
- Wayne 写操作前还会查询 Wayne `operator-permissions`,确认当前登录用户在目标 namespace 下具备创建/更新/删除用户角色的权限。
- 用户首次加入业务线时,如果该业务线绑定了 Wayne namespace,会自动给该用户初始化 Wayne namespace `访客` 角色。
接口列表:
```text
GET /auth/api/v1/subsystem-auth/systems
GET /auth/api/v1/subsystem-auth/wayne/roles
GET /auth/api/v1/subsystem-auth/wayne/business-lines/:id/namespaces
GET /auth/api/v1/subsystem-auth/wayne/users/:username/roles
PUT /auth/api/v1/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles
DELETE /auth/api/v1/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles
POST /auth/api/v1/subsystem-auth/wayne/business-lines/:id/users/:userid/init
```
Wayne 授权示例:
```http
PUT /auth/api/v1/subsystem-auth/wayne/business-lines/1/namespaces/3/users/eastsales@qiniu.com/roles
Authorization: Bearer <authserver_token>
Content-Type: application/json
{
"groupIds": [2],
"replace": true,
"requestId": "req-001",
"reason": "业务线授权"
}
```
Wayne 解绑示例:
```http
DELETE /auth/api/v1/subsystem-auth/wayne/business-lines/1/namespaces/3/users/eastsales@qiniu.com/roles
Authorization: Bearer <authserver_token>
Content-Type: application/json
{
"groupIds": [2],
"requestId": "req-002",
"reason": "回收业务线授权"
}
```
管理员可查看当前 SAML metadata 配置: 管理员可查看当前 SAML metadata 配置:
```text ```text
+81 -3
View File
@@ -4,16 +4,19 @@ import (
"errors" "errors"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/1024XEngineer/xinfra/server/internal/model" "github.com/1024XEngineer/xinfra/server/internal/model"
"github.com/1024XEngineer/xinfra/server/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
type BusinessLineHandler struct { type BusinessLineHandler struct {
db *gorm.DB db *gorm.DB
wayne *service.WayneRoleBindingService
} }
type BusinessLineWithPermission struct { type BusinessLineWithPermission struct {
@@ -45,8 +48,8 @@ type WayneNamespaceBindingItem struct {
KubeNamespace string `json:"kubeNamespace"` KubeNamespace string `json:"kubeNamespace"`
} }
func NewBusinessLineHandler(db *gorm.DB) *BusinessLineHandler { func NewBusinessLineHandler(db *gorm.DB, wayne *service.WayneRoleBindingService) *BusinessLineHandler {
return &BusinessLineHandler{db: db} return &BusinessLineHandler{db: db, wayne: wayne}
} }
func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) { func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) {
@@ -269,6 +272,7 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
} }
var binding model.BusinessLineUser var binding model.BusinessLineUser
created := false
err := h.db.Where("business_line_id = ? AND user_id = ?", req.TargetBusinessLineID, req.TargetUserID).First(&binding).Error err := h.db.Where("business_line_id = ? AND user_id = ?", req.TargetBusinessLineID, req.TargetUserID).First(&binding).Error
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
binding = model.BusinessLineUser{ binding = model.BusinessLineUser{
@@ -280,6 +284,7 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return return
} }
created = true
} else if err != nil { } else if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -291,6 +296,24 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
binding.Permission = req.Permission binding.Permission = req.Permission
} }
var initializedWayne []gin.H
if created {
operatorEmail, ok := subsystemOperatorEmail(c, claims)
if !ok {
return
}
targetUsername := wayneUsernameForUser(targetUser)
if targetUsername == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "target user has no Wayne username"})
return
}
initialized, ok := h.initializeWayneVisitorForBusinessLine(c, req.TargetBusinessLineID, targetUsername, operatorEmail, claims.IsAdmin)
if !ok {
return
}
initializedWayne = initialized
}
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"id": binding.ID, "id": binding.ID,
"business_line_id": binding.BusinessLineID, "business_line_id": binding.BusinessLineID,
@@ -298,6 +321,7 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
"permission": binding.Permission, "permission": binding.Permission,
"created_at": binding.CreatedAt.Format(time.RFC3339), "created_at": binding.CreatedAt.Format(time.RFC3339),
"updated_at": binding.UpdatedAt.Format(time.RFC3339), "updated_at": binding.UpdatedAt.Format(time.RFC3339),
"wayne_init": initializedWayne,
}) })
} }
@@ -399,6 +423,60 @@ func (h *BusinessLineHandler) canManageBusinessLine(c *gin.Context, businessLine
return true return true
} }
func (h *BusinessLineHandler) initializeWayneVisitorForBusinessLine(c *gin.Context, businessLineID uint64, targetUsername string, operatorEmail string, skipWaynePermissionCheck bool) ([]gin.H, bool) {
var namespaces []model.BusinessLineWayneNamespace
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&namespaces).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return nil, false
}
if len(namespaces) == 0 {
return []gin.H{}, true
}
if h.wayne == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "wayne internal role binding api is not configured"})
return nil, false
}
groupIDs, err := h.wayne.NamespaceVisitorGroupIDs(c.Request.Context())
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return nil, false
}
replace := true
req := service.WayneRoleBindingRequest{
GroupIDs: groupIDs,
Replace: &replace,
RequestID: "business-line-user-init-" + strconv.FormatInt(time.Now().UnixNano(), 10),
Reason: "初始化业务线 Wayne 访客角色",
}
items := make([]gin.H, 0, len(namespaces))
for _, namespace := range namespaces {
if !skipWaynePermissionCheck {
permissions, err := h.wayne.NamespaceOperatorPermissionsParsed(c.Request.Context(), namespace.WayneNamespaceID, operatorEmail)
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return nil, false
}
if !permissions.Create && !permissions.Update {
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have Wayne namespace role create or update permission"})
return nil, false
}
}
result, err := h.wayne.BindNamespace(c.Request.Context(), namespace.WayneNamespaceID, strings.TrimSpace(targetUsername), operatorEmail, req)
if err != nil {
writeWayneRoleBindingError(c, result, err)
return nil, false
}
items = append(items, gin.H{
"namespace_id": namespace.WayneNamespaceID,
"namespace_name": namespace.WayneNamespaceName,
"group_ids": groupIDs,
})
}
return items, true
}
func parseBusinessLineID(c *gin.Context) (uint64, bool) { func parseBusinessLineID(c *gin.Context) (uint64, bool) {
value, err := strconv.ParseUint(c.Param("id"), 10, 64) value, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || value == 0 { if err != nil || value == 0 {
+389
View File
@@ -0,0 +1,389 @@
package handler
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/1024XEngineer/xinfra/server/internal/auth"
"github.com/1024XEngineer/xinfra/server/internal/model"
"github.com/1024XEngineer/xinfra/server/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type SubsystemAuthHandler struct {
db *gorm.DB
wayne *service.WayneRoleBindingService
audit *service.AuditService
}
func NewSubsystemAuthHandler(db *gorm.DB, wayne *service.WayneRoleBindingService, audit *service.AuditService) *SubsystemAuthHandler {
return &SubsystemAuthHandler{db: db, wayne: wayne, audit: audit}
}
func (h *SubsystemAuthHandler) ListSystems(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"items": []gin.H{
{"key": "wayne", "name": "Wayne", "enabled": true},
{"key": "clouddm", "name": "CloudDM", "enabled": false},
},
})
}
func (h *SubsystemAuthHandler) ListWayneNamespaceRoles(c *gin.Context) {
groups, err := h.wayne.ListNamespaceRoleGroups(c.Request.Context())
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return
}
c.JSON(http.StatusOK, gin.H{"items": groups})
}
func (h *SubsystemAuthHandler) ListWayneBusinessLineNamespaces(c *gin.Context) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
businessLineID, ok := parseBusinessLineID(c)
if !ok {
return
}
if !h.canManageBusinessLine(c, claims, businessLineID) {
return
}
rows, ok := h.listBusinessLineWayneNamespaces(c, businessLineID)
if !ok {
return
}
operatorEmail, ok := subsystemOperatorEmail(c, claims)
if !ok {
return
}
items := make([]gin.H, 0, len(rows))
for _, row := range rows {
item := gin.H{
"id": row.WayneNamespaceID,
"name": row.WayneNamespaceName,
"kubeNamespace": row.KubeNamespace,
}
if claims.IsAdmin {
item["permissions"] = &service.WayneOperatorPermissions{Create: true, Update: true, Delete: true}
item["can_bind"] = true
item["can_unbind"] = true
} else {
permissions, err := h.wayne.NamespaceOperatorPermissionsParsed(c.Request.Context(), row.WayneNamespaceID, operatorEmail)
if err != nil {
item["permission_error"] = err.Error()
} else {
item["permissions"] = permissions
item["can_bind"] = permissions.Create || permissions.Update
item["can_unbind"] = permissions.Delete
}
}
items = append(items, item)
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (h *SubsystemAuthHandler) GetWayneUserRoles(c *gin.Context) {
username := strings.TrimSpace(c.Param("username"))
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid username"})
return
}
result, err := h.wayne.GetUserRoles(c.Request.Context(), username)
if err != nil {
writeWayneRoleBindingError(c, result, err)
return
}
writeWayneRoleBindingResult(c, result)
}
func (h *SubsystemAuthHandler) BindWayneNamespaceRoles(c *gin.Context) {
h.handleWayneNamespaceRoles(c, http.MethodPut)
}
func (h *SubsystemAuthHandler) UnbindWayneNamespaceRoles(c *gin.Context) {
h.handleWayneNamespaceRoles(c, http.MethodDelete)
}
func (h *SubsystemAuthHandler) InitWayneBusinessLineUser(c *gin.Context) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
businessLineID, ok := parseBusinessLineID(c)
if !ok {
return
}
targetUserID, ok := parseUintPathParam(c, "userid")
if !ok {
return
}
if !h.canManageBusinessLine(c, claims, businessLineID) {
return
}
var target model.User
if err := h.db.Where("id = ? AND deleted_at IS NULL", targetUserID).First(&target).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "target user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
operatorEmail, ok := subsystemOperatorEmail(c, claims)
if !ok {
return
}
targetUsername := wayneUsernameForUser(target)
result, ok := h.initializeWayneVisitor(c, businessLineID, targetUsername, operatorEmail, claims.IsAdmin, service.WayneRoleBindingRequest{
RequestID: "business-line-user-init-" + strconv.FormatUint(targetUserID, 10) + "-" + strconv.FormatInt(time.Now().Unix(), 10),
Reason: "初始化业务线 Wayne 访客角色",
})
if !ok {
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "items": result})
}
func (h *SubsystemAuthHandler) handleWayneNamespaceRoles(c *gin.Context, method string) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
businessLineID, ok := parseBusinessLineID(c)
if !ok {
return
}
namespaceID, ok := parseUintPathParam(c, "namespaceid")
if !ok {
return
}
targetUsername := strings.TrimSpace(c.Param("username"))
if targetUsername == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid username"})
return
}
if !h.canManageBusinessLine(c, claims, businessLineID) {
return
}
if !h.ensureNamespaceBelongsToBusinessLine(c, businessLineID, namespaceID) {
return
}
if !h.ensureLocalUserExists(c, targetUsername) {
return
}
operatorEmail, ok := subsystemOperatorEmail(c, claims)
if !ok {
return
}
if !claims.IsAdmin && !h.ensureWayneOperatorPermission(c, namespaceID, operatorEmail, method) {
return
}
req, ok := parseRoleBindingRequest(c)
if !ok {
return
}
req.Username = ""
if method == http.MethodPut && len(req.GroupIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "groupIds is required"})
return
}
var result *service.WayneRoleBindingResult
var err error
if method == http.MethodPut {
result, err = h.wayne.BindNamespace(c.Request.Context(), namespaceID, targetUsername, operatorEmail, req)
} else {
result, err = h.wayne.UnbindNamespace(c.Request.Context(), namespaceID, targetUsername, operatorEmail, req)
}
if err != nil {
h.writeAudit(c, claims, businessLineID, namespaceID, targetUsername, method, "deny", req.RequestID, err.Error())
writeWayneRoleBindingError(c, result, err)
return
}
h.writeAudit(c, claims, businessLineID, namespaceID, targetUsername, method, "allow", req.RequestID, "")
writeWayneRoleBindingResult(c, result)
}
func (h *SubsystemAuthHandler) canManageBusinessLine(c *gin.Context, claims *auth.Claims, businessLineID uint64) bool {
var businessLine model.BusinessLine
if err := h.db.First(&businessLine, "id = ?", businessLineID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
if claims.IsAdmin {
return true
}
var binding model.BusinessLineUser
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", businessLineID, claims.UserID, 0).
First(&binding).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *SubsystemAuthHandler) ensureNamespaceBelongsToBusinessLine(c *gin.Context, businessLineID, namespaceID uint64) bool {
var row model.BusinessLineWayneNamespace
if err := h.db.Where("business_line_id = ? AND wayne_namespace_id = ?", businessLineID, namespaceID).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusForbidden, gin.H{"error": "wayne namespace is not bound to current business line"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *SubsystemAuthHandler) ensureLocalUserExists(c *gin.Context, username string) bool {
var user model.User
if err := h.db.Where("(username = ? OR email = ?) AND deleted_at IS NULL", username, username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "target user not found"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *SubsystemAuthHandler) ensureWayneOperatorPermission(c *gin.Context, namespaceID uint64, operatorEmail string, method string) bool {
permissions, err := h.wayne.NamespaceOperatorPermissionsParsed(c.Request.Context(), namespaceID, operatorEmail)
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return false
}
if method == http.MethodDelete {
if permissions.Delete {
return true
}
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have Wayne namespace role delete permission"})
return false
}
if permissions.Create || permissions.Update {
return true
}
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have Wayne namespace role create or update permission"})
return false
}
func (h *SubsystemAuthHandler) listBusinessLineWayneNamespaces(c *gin.Context, businessLineID uint64) ([]model.BusinessLineWayneNamespace, bool) {
var rows []model.BusinessLineWayneNamespace
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&rows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return nil, false
}
return rows, true
}
func (h *SubsystemAuthHandler) initializeWayneVisitor(c *gin.Context, businessLineID uint64, targetUsername string, operatorEmail string, skipWaynePermissionCheck bool, req service.WayneRoleBindingRequest) ([]gin.H, bool) {
namespaces, ok := h.listBusinessLineWayneNamespaces(c, businessLineID)
if !ok {
return nil, false
}
if len(namespaces) == 0 {
return []gin.H{}, true
}
groupIDs, err := h.wayne.NamespaceVisitorGroupIDs(c.Request.Context())
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return nil, false
}
req.GroupIDs = groupIDs
replace := true
req.Replace = &replace
items := make([]gin.H, 0, len(namespaces))
for _, namespace := range namespaces {
if !skipWaynePermissionCheck && !h.ensureWayneOperatorPermission(c, namespace.WayneNamespaceID, operatorEmail, http.MethodPut) {
return nil, false
}
result, err := h.wayne.BindNamespace(c.Request.Context(), namespace.WayneNamespaceID, targetUsername, operatorEmail, req)
if err != nil {
writeWayneRoleBindingError(c, result, err)
return nil, false
}
items = append(items, gin.H{
"namespace_id": namespace.WayneNamespaceID,
"namespace_name": namespace.WayneNamespaceName,
"group_ids": groupIDs,
})
}
return items, true
}
func (h *SubsystemAuthHandler) writeAudit(c *gin.Context, claims *auth.Claims, businessLineID, namespaceID uint64, targetUsername, method, decision, requestID, reason string) {
h.audit.Write(service.AuditEntry{
RequestID: requestID,
ActorUserID: claims.UserID,
ActorUsername: actorNameFromClaims(claims),
ClientIP: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
Action: "subsystem_auth.wayne." + strings.ToLower(method) + "." + decision,
ResourceType: "wayne_namespace",
ResourceID: strconv.FormatUint(namespaceID, 10),
ScopeType: "business_line",
ScopeID: businessLineID,
BusinessLineID: businessLineID,
NamespaceID: namespaceID,
Decision: decision,
Reason: truncateAuditReason(reason),
Metadata: map[string]any{
"targetUsername": targetUsername,
},
})
}
func subsystemOperatorEmail(c *gin.Context, claims *auth.Claims) (string, bool) {
operatorEmail := strings.TrimSpace(claims.Email)
if operatorEmail == "" {
operatorEmail = strings.TrimSpace(claims.Username)
}
if operatorEmail == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "username is missing in token"})
return "", false
}
return operatorEmail, true
}
func actorNameFromClaims(claims *auth.Claims) string {
if claims == nil {
return ""
}
if value := strings.TrimSpace(claims.Email); value != "" {
return value
}
return claims.Username
}
func wayneUsernameForUser(user model.User) string {
if value := strings.TrimSpace(user.Email); value != "" {
return value
}
return strings.TrimSpace(user.Username)
}
+24 -28
View File
@@ -64,11 +64,12 @@ func (h *WayneRoleBindingHandler) ListGroups(c *gin.Context) {
} }
func (h *WayneRoleBindingHandler) GetCurrentUserRoles(c *gin.Context) { func (h *WayneRoleBindingHandler) GetCurrentUserRoles(c *gin.Context) {
userID, ok := parseUintPathParam(c, "userid") username := strings.TrimSpace(c.Param("username"))
if !ok { if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid username"})
return return
} }
h.handleQuery(c, "user_roles", userID, "") h.handleQuery(c, "user_roles", 0, username)
} }
func (h *WayneRoleBindingHandler) NamespaceOperatorPermissions(c *gin.Context) { func (h *WayneRoleBindingHandler) NamespaceOperatorPermissions(c *gin.Context) {
@@ -111,7 +112,7 @@ func (h *WayneRoleBindingHandler) handle(c *gin.Context, scope string, method st
if !ok { if !ok {
return return
} }
targetUserID, ok := roleBindingTargetUserID(c, req) targetUsername, ok := roleBindingTargetUsername(c, req)
if !ok { if !ok {
return return
} }
@@ -120,25 +121,25 @@ func (h *WayneRoleBindingHandler) handle(c *gin.Context, scope string, method st
return return
} }
result, err := h.call(c, scope, method, resourceID, targetUserID, operatorEmail, req) result, err := h.call(c, scope, method, resourceID, targetUsername, operatorEmail, req)
if err != nil { if err != nil {
h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, targetUserID, "deny", req.RequestID, err.Error()) h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, targetUsername, "deny", req.RequestID, err.Error())
writeWayneRoleBindingError(c, result, err) writeWayneRoleBindingError(c, result, err)
return return
} }
h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, targetUserID, "allow", req.RequestID, "") h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, targetUsername, "allow", req.RequestID, "")
writeWayneRoleBindingResult(c, result) writeWayneRoleBindingResult(c, result)
} }
func (h *WayneRoleBindingHandler) handleQuery(c *gin.Context, resourceType string, resourceID uint64, _ string) { func (h *WayneRoleBindingHandler) handleQuery(c *gin.Context, resourceType string, resourceID uint64, username string) {
var result *service.WayneRoleBindingResult var result *service.WayneRoleBindingResult
var err error var err error
switch resourceType { switch resourceType {
case "namespaces": case "namespaces":
result, err = h.wayne.ListNamespaces(c.Request.Context()) result, err = h.wayne.ListNamespaces(c.Request.Context())
case "user_roles": case "user_roles":
result, err = h.wayne.GetUserRoles(c.Request.Context(), resourceID) result, err = h.wayne.GetUserRoles(c.Request.Context(), username)
default: default:
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported query resource"}) c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported query resource"})
return return
@@ -181,17 +182,17 @@ func (h *WayneRoleBindingHandler) handleOperatorPermissions(c *gin.Context, scop
writeWayneRoleBindingResult(c, result) writeWayneRoleBindingResult(c, result)
} }
func (h *WayneRoleBindingHandler) call(c *gin.Context, scope, method string, resourceID uint64, targetUserID uint64, operatorEmail string, req service.WayneRoleBindingRequest) (*service.WayneRoleBindingResult, error) { func (h *WayneRoleBindingHandler) call(c *gin.Context, scope, method string, resourceID uint64, targetUsername string, operatorEmail string, req service.WayneRoleBindingRequest) (*service.WayneRoleBindingResult, error) {
if scope == "namespace" { if scope == "namespace" {
if method == http.MethodPut { if method == http.MethodPut {
return h.wayne.BindNamespace(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) return h.wayne.BindNamespace(c.Request.Context(), resourceID, targetUsername, operatorEmail, req)
} }
return h.wayne.UnbindNamespace(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) return h.wayne.UnbindNamespace(c.Request.Context(), resourceID, targetUsername, operatorEmail, req)
} }
if method == http.MethodPut { if method == http.MethodPut {
return h.wayne.BindApp(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) return h.wayne.BindApp(c.Request.Context(), resourceID, targetUsername, operatorEmail, req)
} }
return h.wayne.UnbindApp(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) return h.wayne.UnbindApp(c.Request.Context(), resourceID, targetUsername, operatorEmail, req)
} }
func parseRoleBindingRequest(c *gin.Context) (service.WayneRoleBindingRequest, bool) { func parseRoleBindingRequest(c *gin.Context) (service.WayneRoleBindingRequest, bool) {
@@ -212,24 +213,19 @@ func parseRoleBindingRequest(c *gin.Context) (service.WayneRoleBindingRequest, b
return req, true return req, true
} }
func roleBindingTargetUserID(c *gin.Context, req service.WayneRoleBindingRequest) (uint64, bool) { func roleBindingTargetUsername(c *gin.Context, req service.WayneRoleBindingRequest) (string, bool) {
if req.UserID != nil && *req.UserID != 0 { if username := strings.TrimSpace(req.Username); username != "" {
return *req.UserID, true return username, true
} }
for _, key := range []string{"userId", "user_id", "userid"} { for _, key := range []string{"username", "userName", "user_name"} {
raw := strings.TrimSpace(c.Query(key)) raw := strings.TrimSpace(c.Query(key))
if raw == "" { if raw == "" {
continue continue
} }
value, err := strconv.ParseUint(raw, 10, 64) return raw, true
if err != nil || value == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid userId"})
return 0, false
}
return value, true
} }
c.JSON(http.StatusBadRequest, gin.H{"error": "userId is required"}) c.JSON(http.StatusBadRequest, gin.H{"error": "username is required"})
return 0, false return "", false
} }
func parseUintPathParam(c *gin.Context, name string) (uint64, bool) { func parseUintPathParam(c *gin.Context, name string) (uint64, bool) {
@@ -280,7 +276,7 @@ func writeWayneRoleBindingError(c *gin.Context, result *service.WayneRoleBinding
} }
} }
func (h *WayneRoleBindingHandler) writeAudit(c *gin.Context, userID uint64, operatorEmail, scope string, resourceID uint64, targetUserID uint64, decision, requestID, reason string) { func (h *WayneRoleBindingHandler) writeAudit(c *gin.Context, userID uint64, operatorEmail, scope string, resourceID uint64, targetUsername string, decision, requestID, reason string) {
h.audit.Write(service.AuditEntry{ h.audit.Write(service.AuditEntry{
RequestID: requestID, RequestID: requestID,
ActorUserID: userID, ActorUserID: userID,
@@ -295,7 +291,7 @@ func (h *WayneRoleBindingHandler) writeAudit(c *gin.Context, userID uint64, oper
Decision: decision, Decision: decision,
Reason: truncateAuditReason(reason), Reason: truncateAuditReason(reason),
Metadata: map[string]any{ Metadata: map[string]any{
"targetUserId": targetUserID, "targetUsername": targetUsername,
}, },
}) })
} }
+10 -2
View File
@@ -74,9 +74,10 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
healthHandler := handler.NewHealthHandler(deps.DB) healthHandler := handler.NewHealthHandler(deps.DB)
authHandler := handler.NewAuthHandler(deps.Config, authService) authHandler := handler.NewAuthHandler(deps.Config, authService)
userHandler := handler.NewUserHandler(deps.DB) userHandler := handler.NewUserHandler(deps.DB)
businessLineHandler := handler.NewBusinessLineHandler(deps.DB) businessLineHandler := handler.NewBusinessLineHandler(deps.DB, wayneRoleBindingService)
wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService) wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService)
wayneRoleBindingHandler := handler.NewWayneRoleBindingHandler(wayneRoleBindingService, auditService) wayneRoleBindingHandler := handler.NewWayneRoleBindingHandler(wayneRoleBindingService, auditService)
subsystemAuthHandler := handler.NewSubsystemAuthHandler(deps.DB, wayneRoleBindingService, auditService)
clouddmHandler := handler.NewCloudDMHandler(deps.Config, auditService) clouddmHandler := handler.NewCloudDMHandler(deps.Config, auditService)
samlHandler := handler.NewSAMLHandler(deps.Config, authService) samlHandler := handler.NewSAMLHandler(deps.Config, authService)
oauthHandler := handler.NewOAuthHandler(deps.Config, deps.DB, auditService) oauthHandler := handler.NewOAuthHandler(deps.Config, deps.DB, auditService)
@@ -115,13 +116,20 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
protected.PUT("/wayen/credential", wayenHandler.SaveCredential) protected.PUT("/wayen/credential", wayenHandler.SaveCredential)
protected.GET("/wayne/namespaces", wayneRoleBindingHandler.ListNamespaces) protected.GET("/wayne/namespaces", wayneRoleBindingHandler.ListNamespaces)
protected.GET("/wayne/groups", wayneRoleBindingHandler.ListGroups) protected.GET("/wayne/groups", wayneRoleBindingHandler.ListGroups)
protected.GET("/wayne/users/:userid/roles", wayneRoleBindingHandler.GetCurrentUserRoles) protected.GET("/wayne/users/:username/roles", wayneRoleBindingHandler.GetCurrentUserRoles)
protected.GET("/wayne/namespaces/:namespaceid/operator-permissions", wayneRoleBindingHandler.NamespaceOperatorPermissions) protected.GET("/wayne/namespaces/:namespaceid/operator-permissions", wayneRoleBindingHandler.NamespaceOperatorPermissions)
protected.GET("/wayne/apps/:appid/operator-permissions", wayneRoleBindingHandler.AppOperatorPermissions) protected.GET("/wayne/apps/:appid/operator-permissions", wayneRoleBindingHandler.AppOperatorPermissions)
protected.PUT("/wayne/namespaces/:namespaceid/roles", wayneRoleBindingHandler.BindNamespace) protected.PUT("/wayne/namespaces/:namespaceid/roles", wayneRoleBindingHandler.BindNamespace)
protected.DELETE("/wayne/namespaces/:namespaceid/roles", wayneRoleBindingHandler.UnbindNamespace) protected.DELETE("/wayne/namespaces/:namespaceid/roles", wayneRoleBindingHandler.UnbindNamespace)
protected.PUT("/wayne/apps/:appid/roles", wayneRoleBindingHandler.BindApp) protected.PUT("/wayne/apps/:appid/roles", wayneRoleBindingHandler.BindApp)
protected.DELETE("/wayne/apps/:appid/roles", wayneRoleBindingHandler.UnbindApp) protected.DELETE("/wayne/apps/:appid/roles", wayneRoleBindingHandler.UnbindApp)
protected.GET("/subsystem-auth/systems", subsystemAuthHandler.ListSystems)
protected.GET("/subsystem-auth/wayne/roles", subsystemAuthHandler.ListWayneNamespaceRoles)
protected.GET("/subsystem-auth/wayne/business-lines/:id/namespaces", subsystemAuthHandler.ListWayneBusinessLineNamespaces)
protected.GET("/subsystem-auth/wayne/users/:username/roles", subsystemAuthHandler.GetWayneUserRoles)
protected.PUT("/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles", subsystemAuthHandler.BindWayneNamespaceRoles)
protected.DELETE("/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles", subsystemAuthHandler.UnbindWayneNamespaceRoles)
protected.POST("/subsystem-auth/wayne/business-lines/:id/users/:userid/init", subsystemAuthHandler.InitWayneBusinessLineUser)
protected.GET("/clouddm/login", clouddmHandler.Login) protected.GET("/clouddm/login", clouddmHandler.Login)
} }
} }
+103 -14
View File
@@ -24,7 +24,7 @@ var (
) )
type WayneRoleBindingRequest struct { type WayneRoleBindingRequest struct {
UserID *uint64 `json:"userId,omitempty"` Username string `json:"username,omitempty"`
GroupIDs []uint64 `json:"groupIds,omitempty"` GroupIDs []uint64 `json:"groupIds,omitempty"`
OperatorUserID *uint64 `json:"operatorUserId,omitempty"` OperatorUserID *uint64 `json:"operatorUserId,omitempty"`
OperatorName string `json:"operatorName,omitempty"` OperatorName string `json:"operatorName,omitempty"`
@@ -40,6 +40,19 @@ type WayneRoleBindingResult struct {
Body []byte Body []byte
} }
type WayneRoleGroup struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Comment string `json:"comment"`
Type int `json:"type"`
}
type WayneOperatorPermissions struct {
Create bool `json:"create"`
Update bool `json:"update"`
Delete bool `json:"delete"`
}
type WayneRoleBindingHTTPError struct { type WayneRoleBindingHTTPError struct {
StatusCode int StatusCode int
Body []byte Body []byte
@@ -72,20 +85,20 @@ func NewWayneRoleBindingService(cfg config.Config) *WayneRoleBindingService {
} }
} }
func (s *WayneRoleBindingService) BindNamespace(ctx context.Context, namespaceID uint64, userID uint64, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) BindNamespace(ctx context.Context, namespaceID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%d/roles", namespaceID, userID), operatorEmail, req) return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%s/roles", namespaceID, url.PathEscape(username)), operatorEmail, req)
} }
func (s *WayneRoleBindingService) UnbindNamespace(ctx context.Context, namespaceID uint64, userID uint64, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) UnbindNamespace(ctx context.Context, namespaceID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%d/roles", namespaceID, userID), operatorEmail, req) return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%s/roles", namespaceID, url.PathEscape(username)), operatorEmail, req)
} }
func (s *WayneRoleBindingService) BindApp(ctx context.Context, appID uint64, userID uint64, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) BindApp(ctx context.Context, appID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/apps/%d/users/%d/roles", appID, userID), operatorEmail, req) return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/apps/%d/users/%s/roles", appID, url.PathEscape(username)), operatorEmail, req)
} }
func (s *WayneRoleBindingService) UnbindApp(ctx context.Context, appID uint64, userID uint64, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) UnbindApp(ctx context.Context, appID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/apps/%d/users/%d/roles", appID, userID), operatorEmail, req) return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/apps/%d/users/%s/roles", appID, url.PathEscape(username)), operatorEmail, req)
} }
func (s *WayneRoleBindingService) ListNamespaces(ctx context.Context) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) ListNamespaces(ctx context.Context) (*WayneRoleBindingResult, error) {
@@ -102,17 +115,52 @@ func (s *WayneRoleBindingService) ListGroups(ctx context.Context, groupType *int
return s.callRaw(ctx, http.MethodGet, internalPath, nil) return s.callRaw(ctx, http.MethodGet, internalPath, nil)
} }
func (s *WayneRoleBindingService) GetUserRoles(ctx context.Context, userID uint64) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) ListNamespaceRoleGroups(ctx context.Context) ([]WayneRoleGroup, error) {
if userID == 0 { groupType := 1
return nil, ErrWayneRoleBindingRequestFailed result, err := s.ListGroups(ctx, &groupType)
if err != nil {
return nil, err
} }
return s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/internal/users/%d/roles", userID), nil) return parseWayneRoleGroups(result.Body)
}
func (s *WayneRoleBindingService) NamespaceVisitorGroupIDs(ctx context.Context) ([]uint64, error) {
groups, err := s.ListNamespaceRoleGroups(ctx)
if err != nil {
return nil, err
}
ids := make([]uint64, 0, 1)
for _, group := range groups {
if isWayneVisitorRoleName(group.Name) {
ids = append(ids, group.ID)
}
}
if len(ids) == 0 {
return nil, fmt.Errorf("wayne visitor role group not found")
}
return ids, nil
}
func (s *WayneRoleBindingService) GetUserRoles(ctx context.Context, username string) (*WayneRoleBindingResult, error) {
username = strings.TrimSpace(username)
if username == "" {
return nil, ErrWayenEmailMissing
}
return s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/internal/users/%s/roles", url.PathEscape(username)), nil)
} }
func (s *WayneRoleBindingService) NamespaceOperatorPermissions(ctx context.Context, namespaceID uint64, operatorEmail string) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) NamespaceOperatorPermissions(ctx context.Context, namespaceID uint64, operatorEmail string) (*WayneRoleBindingResult, error) {
return s.operatorPermissions(ctx, fmt.Sprintf("/api/v1/internal/namespaces/%d/operator-permissions", namespaceID), operatorEmail) return s.operatorPermissions(ctx, fmt.Sprintf("/api/v1/internal/namespaces/%d/operator-permissions", namespaceID), operatorEmail)
} }
func (s *WayneRoleBindingService) NamespaceOperatorPermissionsParsed(ctx context.Context, namespaceID uint64, operatorEmail string) (*WayneOperatorPermissions, error) {
result, err := s.NamespaceOperatorPermissions(ctx, namespaceID, operatorEmail)
if err != nil {
return nil, err
}
return parseWayneOperatorPermissions(result.Body)
}
func (s *WayneRoleBindingService) AppOperatorPermissions(ctx context.Context, appID uint64, operatorEmail string) (*WayneRoleBindingResult, error) { func (s *WayneRoleBindingService) AppOperatorPermissions(ctx context.Context, appID uint64, operatorEmail string) (*WayneRoleBindingResult, error) {
return s.operatorPermissions(ctx, fmt.Sprintf("/api/v1/internal/apps/%d/operator-permissions", appID), operatorEmail) return s.operatorPermissions(ctx, fmt.Sprintf("/api/v1/internal/apps/%d/operator-permissions", appID), operatorEmail)
} }
@@ -128,7 +176,7 @@ func (s *WayneRoleBindingService) call(ctx context.Context, method, internalPath
req.OperatorUserID = nil req.OperatorUserID = nil
req.OperatorName = operatorEmail req.OperatorName = operatorEmail
req.UserID = nil req.Username = ""
body, err := json.Marshal(req) body, err := json.Marshal(req)
if err != nil { if err != nil {
@@ -249,3 +297,44 @@ func truncateForDebugLog(value string, limit int) string {
} }
return value[:limit] + "...(truncated)" return value[:limit] + "...(truncated)"
} }
func parseWayneRoleGroups(body []byte) ([]WayneRoleGroup, error) {
var wrapped struct {
Data []WayneRoleGroup `json:"data"`
Items []WayneRoleGroup `json:"items"`
}
if err := json.Unmarshal(body, &wrapped); err == nil {
if wrapped.Data != nil {
return wrapped.Data, nil
}
if wrapped.Items != nil {
return wrapped.Items, nil
}
}
var direct []WayneRoleGroup
if err := json.Unmarshal(body, &direct); err != nil {
return nil, err
}
return direct, nil
}
func parseWayneOperatorPermissions(body []byte) (*WayneOperatorPermissions, error) {
var wrapped struct {
Data struct {
Permissions WayneOperatorPermissions `json:"permissions"`
} `json:"data"`
Permissions WayneOperatorPermissions `json:"permissions"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return nil, err
}
if wrapped.Data.Permissions != (WayneOperatorPermissions{}) {
return &wrapped.Data.Permissions, nil
}
return &wrapped.Permissions, nil
}
func isWayneVisitorRoleName(name string) bool {
normalized := strings.ToLower(strings.TrimSpace(name))
return normalized == "访客" || normalized == "visitor" || strings.Contains(normalized, "visitor")
}
@@ -40,9 +40,8 @@ func TestWayneRoleBindingServiceBindNamespaceSignsAndOverridesOperator(t *testin
operatorUserID := uint64(123) operatorUserID := uint64(123)
replace := false replace := false
targetUserID := uint64(2001) result, err := svc.BindNamespace(context.Background(), 1, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{
result, err := svc.BindNamespace(context.Background(), 1, targetUserID, "eastsales@qiniu.com", WayneRoleBindingRequest{ Username: "target@example.com",
UserID: &targetUserID,
GroupIDs: []uint64{10, 11}, GroupIDs: []uint64{10, 11},
OperatorUserID: &operatorUserID, OperatorUserID: &operatorUserID,
OperatorName: "attacker@example.com", OperatorName: "attacker@example.com",
@@ -56,7 +55,7 @@ func TestWayneRoleBindingServiceBindNamespaceSignsAndOverridesOperator(t *testin
if result.StatusCode != http.StatusOK { if result.StatusCode != http.StatusOK {
t.Fatalf("StatusCode = %d, want 200", result.StatusCode) t.Fatalf("StatusCode = %d, want 200", result.StatusCode)
} }
if requestPath != "/api/v1/internal/namespaces/1/users/2001/roles" { if requestPath != "/api/v1/internal/namespaces/1/users/target@example.com/roles" {
t.Fatalf("requestPath = %q", requestPath) t.Fatalf("requestPath = %q", requestPath)
} }
if payload.OperatorName != "eastsales@qiniu.com" { if payload.OperatorName != "eastsales@qiniu.com" {
@@ -65,8 +64,8 @@ func TestWayneRoleBindingServiceBindNamespaceSignsAndOverridesOperator(t *testin
if payload.OperatorUserID != nil { if payload.OperatorUserID != nil {
t.Fatalf("OperatorUserID should be omitted, got %v", *payload.OperatorUserID) t.Fatalf("OperatorUserID should be omitted, got %v", *payload.OperatorUserID)
} }
if payload.UserID != nil { if payload.Username != "" {
t.Fatalf("UserID should be omitted from Wayne body, got %v", *payload.UserID) t.Fatalf("Username should be omitted from Wayne body, got %q", payload.Username)
} }
if payload.Replace == nil || *payload.Replace { if payload.Replace == nil || *payload.Replace {
t.Fatalf("Replace = %v, want false", payload.Replace) t.Fatalf("Replace = %v, want false", payload.Replace)
@@ -82,23 +81,23 @@ func TestWayneRoleBindingServiceCallsAllDocumentedEndpoints(t *testing.T) {
{ {
name: "unbind namespace", name: "unbind namespace",
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
return s.UnbindNamespace(context.Background(), 1, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{10}}) return s.UnbindNamespace(context.Background(), 1, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{10}})
}, },
want: "DELETE /api/v1/internal/namespaces/1/users/2001/roles", want: "DELETE /api/v1/internal/namespaces/1/users/target@example.com/roles",
}, },
{ {
name: "bind app", name: "bind app",
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
return s.BindApp(context.Background(), 3, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) return s.BindApp(context.Background(), 3, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}})
}, },
want: "PUT /api/v1/internal/apps/3/users/2001/roles", want: "PUT /api/v1/internal/apps/3/users/target@example.com/roles",
}, },
{ {
name: "unbind app", name: "unbind app",
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
return s.UnbindApp(context.Background(), 3, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) return s.UnbindApp(context.Background(), 3, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}})
}, },
want: "DELETE /api/v1/internal/apps/3/users/2001/roles", want: "DELETE /api/v1/internal/apps/3/users/target@example.com/roles",
}, },
{ {
name: "list namespaces", name: "list namespaces",
@@ -125,9 +124,9 @@ func TestWayneRoleBindingServiceCallsAllDocumentedEndpoints(t *testing.T) {
{ {
name: "get user roles", name: "get user roles",
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
return s.GetUserRoles(context.Background(), 2001) return s.GetUserRoles(context.Background(), "target@example.com")
}, },
want: "GET /api/v1/internal/users/2001/roles", want: "GET /api/v1/internal/users/target@example.com/roles",
}, },
{ {
name: "namespace operator permissions", name: "namespace operator permissions",
@@ -231,7 +230,7 @@ func TestWayneRoleBindingServiceHTTPError(t *testing.T) {
WayneServiceName: "xinfra", WayneServiceName: "xinfra",
WayneServiceAPISecretKey: "service-secret", WayneServiceAPISecretKey: "service-secret",
}) })
result, err := svc.BindApp(context.Background(), 3, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) result, err := svc.BindApp(context.Background(), 3, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}})
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }