feat(server): add Wayne subsystem authorization APIs
This commit is contained in:
+58
-6
@@ -285,14 +285,14 @@ Wayne 会把回调地址拼成:
|
||||
|
||||
## Wayne 授权代理接口
|
||||
|
||||
AuthServer 的 Wayne 授权代理接口要求调用方传目标 Wayne `userId`。后端会从当前 `authserver_token` 里取 `email` 作为操作者 `operatorName`,目标用户 ID 由请求体或路径参数提供。
|
||||
AuthServer 的 Wayne 授权代理接口要求调用方传目标 Wayne `username`。后端会从当前 `authserver_token` 里取 `email` 作为操作者 `operatorName`,目标用户名由请求体或路径参数提供。
|
||||
|
||||
对外接口:
|
||||
|
||||
```text
|
||||
GET /auth/api/v1/wayne/namespaces
|
||||
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/apps/:appid/operator-permissions
|
||||
PUT /auth/api/v1/wayne/namespaces/:namespaceid/roles
|
||||
@@ -309,7 +309,7 @@ Authorization: Bearer <authserver_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"userId": 2001,
|
||||
"username": "target@example.com",
|
||||
"groupIds": [10, 11],
|
||||
"replace": false,
|
||||
"requestId": "req-001",
|
||||
@@ -317,13 +317,13 @@ Content-Type: application/json
|
||||
}
|
||||
```
|
||||
|
||||
AuthServer 转发到 Wayne internal API 时会使用请求体里的 `userId`:
|
||||
AuthServer 转发到 Wayne internal API 时会使用请求体里的 `username`:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 子系统赋权接口
|
||||
|
||||
子系统赋权接口是平台业务层接口,前端应优先调用这一组,而不是直接调用低层 `/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 配置:
|
||||
|
||||
```text
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
|
||||
type BusinessLineHandler struct {
|
||||
db *gorm.DB
|
||||
wayne *service.WayneRoleBindingService
|
||||
}
|
||||
|
||||
type BusinessLineWithPermission struct {
|
||||
@@ -45,8 +48,8 @@ type WayneNamespaceBindingItem struct {
|
||||
KubeNamespace string `json:"kubeNamespace"`
|
||||
}
|
||||
|
||||
func NewBusinessLineHandler(db *gorm.DB) *BusinessLineHandler {
|
||||
return &BusinessLineHandler{db: db}
|
||||
func NewBusinessLineHandler(db *gorm.DB, wayne *service.WayneRoleBindingService) *BusinessLineHandler {
|
||||
return &BusinessLineHandler{db: db, wayne: wayne}
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) {
|
||||
@@ -269,6 +272,7 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
|
||||
}
|
||||
|
||||
var binding model.BusinessLineUser
|
||||
created := false
|
||||
err := h.db.Where("business_line_id = ? AND user_id = ?", req.TargetBusinessLineID, req.TargetUserID).First(&binding).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
binding = model.BusinessLineUser{
|
||||
@@ -280,6 +284,7 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
created = true
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -291,6 +296,24 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
|
||||
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{
|
||||
"id": binding.ID,
|
||||
"business_line_id": binding.BusinessLineID,
|
||||
@@ -298,6 +321,7 @@ func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
|
||||
"permission": binding.Permission,
|
||||
"created_at": binding.CreatedAt.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
|
||||
}
|
||||
|
||||
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) {
|
||||
value, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -64,11 +64,12 @@ func (h *WayneRoleBindingHandler) ListGroups(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *WayneRoleBindingHandler) GetCurrentUserRoles(c *gin.Context) {
|
||||
userID, ok := parseUintPathParam(c, "userid")
|
||||
if !ok {
|
||||
username := strings.TrimSpace(c.Param("username"))
|
||||
if username == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid username"})
|
||||
return
|
||||
}
|
||||
h.handleQuery(c, "user_roles", userID, "")
|
||||
h.handleQuery(c, "user_roles", 0, username)
|
||||
}
|
||||
|
||||
func (h *WayneRoleBindingHandler) NamespaceOperatorPermissions(c *gin.Context) {
|
||||
@@ -111,7 +112,7 @@ func (h *WayneRoleBindingHandler) handle(c *gin.Context, scope string, method st
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
targetUserID, ok := roleBindingTargetUserID(c, req)
|
||||
targetUsername, ok := roleBindingTargetUsername(c, req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -120,25 +121,25 @@ func (h *WayneRoleBindingHandler) handle(c *gin.Context, scope string, method st
|
||||
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 {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
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 err error
|
||||
switch resourceType {
|
||||
case "namespaces":
|
||||
result, err = h.wayne.ListNamespaces(c.Request.Context())
|
||||
case "user_roles":
|
||||
result, err = h.wayne.GetUserRoles(c.Request.Context(), resourceID)
|
||||
result, err = h.wayne.GetUserRoles(c.Request.Context(), username)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported query resource"})
|
||||
return
|
||||
@@ -181,17 +182,17 @@ func (h *WayneRoleBindingHandler) handleOperatorPermissions(c *gin.Context, scop
|
||||
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 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 {
|
||||
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) {
|
||||
@@ -212,24 +213,19 @@ func parseRoleBindingRequest(c *gin.Context) (service.WayneRoleBindingRequest, b
|
||||
return req, true
|
||||
}
|
||||
|
||||
func roleBindingTargetUserID(c *gin.Context, req service.WayneRoleBindingRequest) (uint64, bool) {
|
||||
if req.UserID != nil && *req.UserID != 0 {
|
||||
return *req.UserID, true
|
||||
func roleBindingTargetUsername(c *gin.Context, req service.WayneRoleBindingRequest) (string, bool) {
|
||||
if username := strings.TrimSpace(req.Username); username != "" {
|
||||
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))
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid userId"})
|
||||
return 0, false
|
||||
return raw, true
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "userId is required"})
|
||||
return 0, false
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "username is required"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
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{
|
||||
RequestID: requestID,
|
||||
ActorUserID: userID,
|
||||
@@ -295,7 +291,7 @@ func (h *WayneRoleBindingHandler) writeAudit(c *gin.Context, userID uint64, oper
|
||||
Decision: decision,
|
||||
Reason: truncateAuditReason(reason),
|
||||
Metadata: map[string]any{
|
||||
"targetUserId": targetUserID,
|
||||
"targetUsername": targetUsername,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,9 +74,10 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
healthHandler := handler.NewHealthHandler(deps.DB)
|
||||
authHandler := handler.NewAuthHandler(deps.Config, authService)
|
||||
userHandler := handler.NewUserHandler(deps.DB)
|
||||
businessLineHandler := handler.NewBusinessLineHandler(deps.DB)
|
||||
businessLineHandler := handler.NewBusinessLineHandler(deps.DB, wayneRoleBindingService)
|
||||
wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService)
|
||||
wayneRoleBindingHandler := handler.NewWayneRoleBindingHandler(wayneRoleBindingService, auditService)
|
||||
subsystemAuthHandler := handler.NewSubsystemAuthHandler(deps.DB, wayneRoleBindingService, auditService)
|
||||
clouddmHandler := handler.NewCloudDMHandler(deps.Config, auditService)
|
||||
samlHandler := handler.NewSAMLHandler(deps.Config, authService)
|
||||
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.GET("/wayne/namespaces", wayneRoleBindingHandler.ListNamespaces)
|
||||
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/apps/:appid/operator-permissions", wayneRoleBindingHandler.AppOperatorPermissions)
|
||||
protected.PUT("/wayne/namespaces/:namespaceid/roles", wayneRoleBindingHandler.BindNamespace)
|
||||
protected.DELETE("/wayne/namespaces/:namespaceid/roles", wayneRoleBindingHandler.UnbindNamespace)
|
||||
protected.PUT("/wayne/apps/:appid/roles", wayneRoleBindingHandler.BindApp)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ var (
|
||||
)
|
||||
|
||||
type WayneRoleBindingRequest struct {
|
||||
UserID *uint64 `json:"userId,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
GroupIDs []uint64 `json:"groupIds,omitempty"`
|
||||
OperatorUserID *uint64 `json:"operatorUserId,omitempty"`
|
||||
OperatorName string `json:"operatorName,omitempty"`
|
||||
@@ -40,6 +40,19 @@ type WayneRoleBindingResult struct {
|
||||
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 {
|
||||
StatusCode int
|
||||
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) {
|
||||
return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%d/roles", namespaceID, userID), operatorEmail, req)
|
||||
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/%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) {
|
||||
return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%d/roles", namespaceID, userID), operatorEmail, req)
|
||||
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/%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) {
|
||||
return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/apps/%d/users/%d/roles", appID, userID), operatorEmail, req)
|
||||
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/%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) {
|
||||
return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/apps/%d/users/%d/roles", appID, userID), operatorEmail, req)
|
||||
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/%s/roles", appID, url.PathEscape(username)), operatorEmail, req)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) GetUserRoles(ctx context.Context, userID uint64) (*WayneRoleBindingResult, error) {
|
||||
if userID == 0 {
|
||||
return nil, ErrWayneRoleBindingRequestFailed
|
||||
func (s *WayneRoleBindingService) ListNamespaceRoleGroups(ctx context.Context) ([]WayneRoleGroup, error) {
|
||||
groupType := 1
|
||||
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) {
|
||||
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) {
|
||||
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.OperatorName = operatorEmail
|
||||
req.UserID = nil
|
||||
req.Username = ""
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
@@ -249,3 +297,44 @@ func truncateForDebugLog(value string, limit int) string {
|
||||
}
|
||||
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)
|
||||
replace := false
|
||||
targetUserID := uint64(2001)
|
||||
result, err := svc.BindNamespace(context.Background(), 1, targetUserID, "eastsales@qiniu.com", WayneRoleBindingRequest{
|
||||
UserID: &targetUserID,
|
||||
result, err := svc.BindNamespace(context.Background(), 1, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{
|
||||
Username: "target@example.com",
|
||||
GroupIDs: []uint64{10, 11},
|
||||
OperatorUserID: &operatorUserID,
|
||||
OperatorName: "attacker@example.com",
|
||||
@@ -56,7 +55,7 @@ func TestWayneRoleBindingServiceBindNamespaceSignsAndOverridesOperator(t *testin
|
||||
if result.StatusCode != http.StatusOK {
|
||||
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)
|
||||
}
|
||||
if payload.OperatorName != "eastsales@qiniu.com" {
|
||||
@@ -65,8 +64,8 @@ func TestWayneRoleBindingServiceBindNamespaceSignsAndOverridesOperator(t *testin
|
||||
if payload.OperatorUserID != nil {
|
||||
t.Fatalf("OperatorUserID should be omitted, got %v", *payload.OperatorUserID)
|
||||
}
|
||||
if payload.UserID != nil {
|
||||
t.Fatalf("UserID should be omitted from Wayne body, got %v", *payload.UserID)
|
||||
if payload.Username != "" {
|
||||
t.Fatalf("Username should be omitted from Wayne body, got %q", payload.Username)
|
||||
}
|
||||
if payload.Replace == nil || *payload.Replace {
|
||||
t.Fatalf("Replace = %v, want false", payload.Replace)
|
||||
@@ -82,23 +81,23 @@ func TestWayneRoleBindingServiceCallsAllDocumentedEndpoints(t *testing.T) {
|
||||
{
|
||||
name: "unbind namespace",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
@@ -125,9 +124,9 @@ func TestWayneRoleBindingServiceCallsAllDocumentedEndpoints(t *testing.T) {
|
||||
{
|
||||
name: "get user roles",
|
||||
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",
|
||||
@@ -231,7 +230,7 @@ func TestWayneRoleBindingServiceHTTPError(t *testing.T) {
|
||||
WayneServiceName: "xinfra",
|
||||
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 {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user