feat(server): add Wayne subsystem authorization APIs

This commit is contained in:
mac
2026-07-17 19:26:40 +08:00
parent add4b1566e
commit 8d49230247
7 changed files with 679 additions and 68 deletions
+81 -3
View File
@@ -4,16 +4,19 @@ 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"
)
type BusinessLineHandler struct {
db *gorm.DB
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 {
+389
View File
@@ -0,0 +1,389 @@
package handler
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/1024XEngineer/xinfra/server/internal/auth"
"github.com/1024XEngineer/xinfra/server/internal/model"
"github.com/1024XEngineer/xinfra/server/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type SubsystemAuthHandler struct {
db *gorm.DB
wayne *service.WayneRoleBindingService
audit *service.AuditService
}
func NewSubsystemAuthHandler(db *gorm.DB, wayne *service.WayneRoleBindingService, audit *service.AuditService) *SubsystemAuthHandler {
return &SubsystemAuthHandler{db: db, wayne: wayne, audit: audit}
}
func (h *SubsystemAuthHandler) ListSystems(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"items": []gin.H{
{"key": "wayne", "name": "Wayne", "enabled": true},
{"key": "clouddm", "name": "CloudDM", "enabled": false},
},
})
}
func (h *SubsystemAuthHandler) ListWayneNamespaceRoles(c *gin.Context) {
groups, err := h.wayne.ListNamespaceRoleGroups(c.Request.Context())
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return
}
c.JSON(http.StatusOK, gin.H{"items": groups})
}
func (h *SubsystemAuthHandler) ListWayneBusinessLineNamespaces(c *gin.Context) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
businessLineID, ok := parseBusinessLineID(c)
if !ok {
return
}
if !h.canManageBusinessLine(c, claims, businessLineID) {
return
}
rows, ok := h.listBusinessLineWayneNamespaces(c, businessLineID)
if !ok {
return
}
operatorEmail, ok := subsystemOperatorEmail(c, claims)
if !ok {
return
}
items := make([]gin.H, 0, len(rows))
for _, row := range rows {
item := gin.H{
"id": row.WayneNamespaceID,
"name": row.WayneNamespaceName,
"kubeNamespace": row.KubeNamespace,
}
if claims.IsAdmin {
item["permissions"] = &service.WayneOperatorPermissions{Create: true, Update: true, Delete: true}
item["can_bind"] = true
item["can_unbind"] = true
} else {
permissions, err := h.wayne.NamespaceOperatorPermissionsParsed(c.Request.Context(), row.WayneNamespaceID, operatorEmail)
if err != nil {
item["permission_error"] = err.Error()
} else {
item["permissions"] = permissions
item["can_bind"] = permissions.Create || permissions.Update
item["can_unbind"] = permissions.Delete
}
}
items = append(items, item)
}
c.JSON(http.StatusOK, gin.H{"items": items})
}
func (h *SubsystemAuthHandler) GetWayneUserRoles(c *gin.Context) {
username := strings.TrimSpace(c.Param("username"))
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid username"})
return
}
result, err := h.wayne.GetUserRoles(c.Request.Context(), username)
if err != nil {
writeWayneRoleBindingError(c, result, err)
return
}
writeWayneRoleBindingResult(c, result)
}
func (h *SubsystemAuthHandler) BindWayneNamespaceRoles(c *gin.Context) {
h.handleWayneNamespaceRoles(c, http.MethodPut)
}
func (h *SubsystemAuthHandler) UnbindWayneNamespaceRoles(c *gin.Context) {
h.handleWayneNamespaceRoles(c, http.MethodDelete)
}
func (h *SubsystemAuthHandler) InitWayneBusinessLineUser(c *gin.Context) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
businessLineID, ok := parseBusinessLineID(c)
if !ok {
return
}
targetUserID, ok := parseUintPathParam(c, "userid")
if !ok {
return
}
if !h.canManageBusinessLine(c, claims, businessLineID) {
return
}
var target model.User
if err := h.db.Where("id = ? AND deleted_at IS NULL", targetUserID).First(&target).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "target user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
operatorEmail, ok := subsystemOperatorEmail(c, claims)
if !ok {
return
}
targetUsername := wayneUsernameForUser(target)
result, ok := h.initializeWayneVisitor(c, businessLineID, targetUsername, operatorEmail, claims.IsAdmin, service.WayneRoleBindingRequest{
RequestID: "business-line-user-init-" + strconv.FormatUint(targetUserID, 10) + "-" + strconv.FormatInt(time.Now().Unix(), 10),
Reason: "初始化业务线 Wayne 访客角色",
})
if !ok {
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "items": result})
}
func (h *SubsystemAuthHandler) handleWayneNamespaceRoles(c *gin.Context, method string) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
businessLineID, ok := parseBusinessLineID(c)
if !ok {
return
}
namespaceID, ok := parseUintPathParam(c, "namespaceid")
if !ok {
return
}
targetUsername := strings.TrimSpace(c.Param("username"))
if targetUsername == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid username"})
return
}
if !h.canManageBusinessLine(c, claims, businessLineID) {
return
}
if !h.ensureNamespaceBelongsToBusinessLine(c, businessLineID, namespaceID) {
return
}
if !h.ensureLocalUserExists(c, targetUsername) {
return
}
operatorEmail, ok := subsystemOperatorEmail(c, claims)
if !ok {
return
}
if !claims.IsAdmin && !h.ensureWayneOperatorPermission(c, namespaceID, operatorEmail, method) {
return
}
req, ok := parseRoleBindingRequest(c)
if !ok {
return
}
req.Username = ""
if method == http.MethodPut && len(req.GroupIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "groupIds is required"})
return
}
var result *service.WayneRoleBindingResult
var err error
if method == http.MethodPut {
result, err = h.wayne.BindNamespace(c.Request.Context(), namespaceID, targetUsername, operatorEmail, req)
} else {
result, err = h.wayne.UnbindNamespace(c.Request.Context(), namespaceID, targetUsername, operatorEmail, req)
}
if err != nil {
h.writeAudit(c, claims, businessLineID, namespaceID, targetUsername, method, "deny", req.RequestID, err.Error())
writeWayneRoleBindingError(c, result, err)
return
}
h.writeAudit(c, claims, businessLineID, namespaceID, targetUsername, method, "allow", req.RequestID, "")
writeWayneRoleBindingResult(c, result)
}
func (h *SubsystemAuthHandler) canManageBusinessLine(c *gin.Context, claims *auth.Claims, businessLineID uint64) bool {
var businessLine model.BusinessLine
if err := h.db.First(&businessLine, "id = ?", businessLineID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
if claims.IsAdmin {
return true
}
var binding model.BusinessLineUser
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", businessLineID, claims.UserID, 0).
First(&binding).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *SubsystemAuthHandler) ensureNamespaceBelongsToBusinessLine(c *gin.Context, businessLineID, namespaceID uint64) bool {
var row model.BusinessLineWayneNamespace
if err := h.db.Where("business_line_id = ? AND wayne_namespace_id = ?", businessLineID, namespaceID).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusForbidden, gin.H{"error": "wayne namespace is not bound to current business line"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *SubsystemAuthHandler) ensureLocalUserExists(c *gin.Context, username string) bool {
var user model.User
if err := h.db.Where("(username = ? OR email = ?) AND deleted_at IS NULL", username, username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "target user not found"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *SubsystemAuthHandler) ensureWayneOperatorPermission(c *gin.Context, namespaceID uint64, operatorEmail string, method string) bool {
permissions, err := h.wayne.NamespaceOperatorPermissionsParsed(c.Request.Context(), namespaceID, operatorEmail)
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return false
}
if method == http.MethodDelete {
if permissions.Delete {
return true
}
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have Wayne namespace role delete permission"})
return false
}
if permissions.Create || permissions.Update {
return true
}
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have Wayne namespace role create or update permission"})
return false
}
func (h *SubsystemAuthHandler) listBusinessLineWayneNamespaces(c *gin.Context, businessLineID uint64) ([]model.BusinessLineWayneNamespace, bool) {
var rows []model.BusinessLineWayneNamespace
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&rows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return nil, false
}
return rows, true
}
func (h *SubsystemAuthHandler) initializeWayneVisitor(c *gin.Context, businessLineID uint64, targetUsername string, operatorEmail string, skipWaynePermissionCheck bool, req service.WayneRoleBindingRequest) ([]gin.H, bool) {
namespaces, ok := h.listBusinessLineWayneNamespaces(c, businessLineID)
if !ok {
return nil, false
}
if len(namespaces) == 0 {
return []gin.H{}, true
}
groupIDs, err := h.wayne.NamespaceVisitorGroupIDs(c.Request.Context())
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return nil, false
}
req.GroupIDs = groupIDs
replace := true
req.Replace = &replace
items := make([]gin.H, 0, len(namespaces))
for _, namespace := range namespaces {
if !skipWaynePermissionCheck && !h.ensureWayneOperatorPermission(c, namespace.WayneNamespaceID, operatorEmail, http.MethodPut) {
return nil, false
}
result, err := h.wayne.BindNamespace(c.Request.Context(), namespace.WayneNamespaceID, targetUsername, operatorEmail, req)
if err != nil {
writeWayneRoleBindingError(c, result, err)
return nil, false
}
items = append(items, gin.H{
"namespace_id": namespace.WayneNamespaceID,
"namespace_name": namespace.WayneNamespaceName,
"group_ids": groupIDs,
})
}
return items, true
}
func (h *SubsystemAuthHandler) writeAudit(c *gin.Context, claims *auth.Claims, businessLineID, namespaceID uint64, targetUsername, method, decision, requestID, reason string) {
h.audit.Write(service.AuditEntry{
RequestID: requestID,
ActorUserID: claims.UserID,
ActorUsername: actorNameFromClaims(claims),
ClientIP: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
Action: "subsystem_auth.wayne." + strings.ToLower(method) + "." + decision,
ResourceType: "wayne_namespace",
ResourceID: strconv.FormatUint(namespaceID, 10),
ScopeType: "business_line",
ScopeID: businessLineID,
BusinessLineID: businessLineID,
NamespaceID: namespaceID,
Decision: decision,
Reason: truncateAuditReason(reason),
Metadata: map[string]any{
"targetUsername": targetUsername,
},
})
}
func subsystemOperatorEmail(c *gin.Context, claims *auth.Claims) (string, bool) {
operatorEmail := strings.TrimSpace(claims.Email)
if operatorEmail == "" {
operatorEmail = strings.TrimSpace(claims.Username)
}
if operatorEmail == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "username is missing in token"})
return "", false
}
return operatorEmail, true
}
func actorNameFromClaims(claims *auth.Claims) string {
if claims == nil {
return ""
}
if value := strings.TrimSpace(claims.Email); value != "" {
return value
}
return claims.Username
}
func wayneUsernameForUser(user model.User) string {
if value := strings.TrimSpace(user.Email); value != "" {
return value
}
return strings.TrimSpace(user.Username)
}
+24 -28
View File
@@ -64,11 +64,12 @@ func (h *WayneRoleBindingHandler) ListGroups(c *gin.Context) {
}
func (h *WayneRoleBindingHandler) GetCurrentUserRoles(c *gin.Context) {
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 value, true
return raw, 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,
},
})
}