feat(server): add Wayne subsystem authorization APIs
This commit is contained in:
+58
-6
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
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,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user