diff --git a/server/README.md b/server/README.md index 8c280b8..3c4492f 100644 --- a/server/README.md +++ b/server/README.md @@ -285,14 +285,14 @@ Wayne 会把回调地址拼成: ## Wayne 授权代理接口 -AuthServer 的 Wayne 授权代理接口不要求调用方传 Wayne user ID。后端会从当前 `authserver_token` 里取 `email`,把它作为 Wayne username 传给 Wayne internal API。 +AuthServer 的 Wayne 授权代理接口要求调用方传目标 Wayne `userId`。后端会从当前 `authserver_token` 里取 `email` 作为操作者 `operatorName`,目标用户 ID 由请求体或路径参数提供。 对外接口: ```text GET /auth/api/v1/wayne/namespaces GET /auth/api/v1/wayne/groups -GET /auth/api/v1/wayne/users/me/roles +GET /auth/api/v1/wayne/users/:userid/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,6 +309,7 @@ Authorization: Bearer Content-Type: application/json { + "userId": 2001, "groupIds": [10, 11], "replace": false, "requestId": "req-001", @@ -316,13 +317,13 @@ Content-Type: application/json } ``` -AuthServer 转发到 Wayne internal API 时会使用 token email: +AuthServer 转发到 Wayne internal API 时会使用请求体里的 `userId`: ```text -PUT /api/v1/internal/namespaces/1/users//roles +PUT /api/v1/internal/namespaces/1/users/2001/roles ``` -并覆盖请求体中的 `operatorName` 为 token email,忽略外部传入的 `operatorUserId`。 +并覆盖请求体中的 `operatorName` 为 token email,忽略外部传入的 `operatorUserId`。`userId` 只用于 Wayne path,不会透传到 Wayne 请求体。 相关配置: diff --git a/server/internal/handler/wayne_role_binding.go b/server/internal/handler/wayne_role_binding.go index 04c036d..cda3ffa 100644 --- a/server/internal/handler/wayne_role_binding.go +++ b/server/internal/handler/wayne_role_binding.go @@ -64,11 +64,11 @@ func (h *WayneRoleBindingHandler) ListGroups(c *gin.Context) { } func (h *WayneRoleBindingHandler) GetCurrentUserRoles(c *gin.Context) { - username, ok := currentTokenEmail(c) + userID, ok := parseUintPathParam(c, "userid") if !ok { return } - h.handleQuery(c, "user_roles", 0, username) + h.handleQuery(c, "user_roles", userID, "") } func (h *WayneRoleBindingHandler) NamespaceOperatorPermissions(c *gin.Context) { @@ -111,30 +111,34 @@ func (h *WayneRoleBindingHandler) handle(c *gin.Context, scope string, method st if !ok { return } + targetUserID, ok := roleBindingTargetUserID(c, req) + if !ok { + return + } if method == http.MethodPut && len(req.GroupIDs) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "groupIds is required"}) return } - result, err := h.call(c, scope, method, resourceID, operatorEmail, req) + result, err := h.call(c, scope, method, resourceID, targetUserID, operatorEmail, req) if err != nil { - h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, operatorEmail, "deny", req.RequestID, err.Error()) + h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, targetUserID, "deny", req.RequestID, err.Error()) writeWayneRoleBindingError(c, result, err) return } - h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, operatorEmail, "allow", req.RequestID, "") + h.writeAudit(c, claims.UserID, operatorEmail, scope, resourceID, targetUserID, "allow", req.RequestID, "") writeWayneRoleBindingResult(c, result) } -func (h *WayneRoleBindingHandler) handleQuery(c *gin.Context, resourceType string, resourceID uint64, username string) { +func (h *WayneRoleBindingHandler) handleQuery(c *gin.Context, resourceType string, resourceID uint64, _ 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(), username) + result, err = h.wayne.GetUserRoles(c.Request.Context(), resourceID) default: c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported query resource"}) return @@ -177,17 +181,17 @@ func (h *WayneRoleBindingHandler) handleOperatorPermissions(c *gin.Context, scop writeWayneRoleBindingResult(c, result) } -func (h *WayneRoleBindingHandler) call(c *gin.Context, scope, method string, resourceID uint64, operatorEmail string, req service.WayneRoleBindingRequest) (*service.WayneRoleBindingResult, error) { +func (h *WayneRoleBindingHandler) call(c *gin.Context, scope, method string, resourceID uint64, targetUserID uint64, operatorEmail string, req service.WayneRoleBindingRequest) (*service.WayneRoleBindingResult, error) { if scope == "namespace" { if method == http.MethodPut { - return h.wayne.BindNamespace(c.Request.Context(), resourceID, operatorEmail, req) + return h.wayne.BindNamespace(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) } - return h.wayne.UnbindNamespace(c.Request.Context(), resourceID, operatorEmail, req) + return h.wayne.UnbindNamespace(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) } if method == http.MethodPut { - return h.wayne.BindApp(c.Request.Context(), resourceID, operatorEmail, req) + return h.wayne.BindApp(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) } - return h.wayne.UnbindApp(c.Request.Context(), resourceID, operatorEmail, req) + return h.wayne.UnbindApp(c.Request.Context(), resourceID, targetUserID, operatorEmail, req) } func parseRoleBindingRequest(c *gin.Context) (service.WayneRoleBindingRequest, bool) { @@ -208,6 +212,26 @@ 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 + } + for _, key := range []string{"userId", "user_id", "userid"} { + 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 + } + c.JSON(http.StatusBadRequest, gin.H{"error": "userId is required"}) + return 0, false +} + func parseUintPathParam(c *gin.Context, name string) (uint64, bool) { raw := strings.TrimSpace(c.Param(name)) value, err := strconv.ParseUint(raw, 10, 64) @@ -256,21 +280,7 @@ func writeWayneRoleBindingError(c *gin.Context, result *service.WayneRoleBinding } } -func currentTokenEmail(c *gin.Context) (string, bool) { - claims, ok := CurrentClaims(c) - if !ok { - c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"}) - return "", false - } - email := strings.TrimSpace(claims.Email) - if email == "" { - c.JSON(http.StatusBadRequest, gin.H{"error": "email is missing in token"}) - return "", false - } - return email, true -} - -func (h *WayneRoleBindingHandler) writeAudit(c *gin.Context, userID uint64, operatorEmail, scope string, resourceID uint64, targetUsername string, decision, requestID, reason string) { +func (h *WayneRoleBindingHandler) writeAudit(c *gin.Context, userID uint64, operatorEmail, scope string, resourceID uint64, targetUserID uint64, decision, requestID, reason string) { h.audit.Write(service.AuditEntry{ RequestID: requestID, ActorUserID: userID, @@ -283,9 +293,9 @@ func (h *WayneRoleBindingHandler) writeAudit(c *gin.Context, userID uint64, oper ScopeType: scope, ScopeID: resourceID, Decision: decision, - Reason: reason, + Reason: truncateAuditReason(reason), Metadata: map[string]any{ - "targetUsername": targetUsername, + "targetUserId": targetUserID, }, }) } @@ -310,6 +320,14 @@ func (h *WayneRoleBindingHandler) writeQueryAudit(c *gin.Context, resourceType s ResourceType: "wayne_" + resourceType, ResourceID: strconv.FormatUint(resourceID, 10), Decision: decision, - Reason: reason, + Reason: truncateAuditReason(reason), }) } + +func truncateAuditReason(reason string) string { + const maxReasonBytes = 512 + if len(reason) <= maxReasonBytes { + return reason + } + return reason[:maxReasonBytes] +} diff --git a/server/internal/router/router.go b/server/internal/router/router.go index b0045b2..4300d49 100644 --- a/server/internal/router/router.go +++ b/server/internal/router/router.go @@ -115,7 +115,7 @@ 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/me/roles", wayneRoleBindingHandler.GetCurrentUserRoles) + protected.GET("/wayne/users/:userid/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) diff --git a/server/internal/service/wayne_role_binding.go b/server/internal/service/wayne_role_binding.go index d331f3f..8da4f91 100644 --- a/server/internal/service/wayne_role_binding.go +++ b/server/internal/service/wayne_role_binding.go @@ -24,6 +24,7 @@ var ( ) type WayneRoleBindingRequest struct { + UserID *uint64 `json:"userId,omitempty"` GroupIDs []uint64 `json:"groupIds,omitempty"` OperatorUserID *uint64 `json:"operatorUserId,omitempty"` OperatorName string `json:"operatorName,omitempty"` @@ -71,20 +72,20 @@ func NewWayneRoleBindingService(cfg config.Config) *WayneRoleBindingService { } } -func (s *WayneRoleBindingService) BindNamespace(ctx context.Context, namespaceID uint64, username 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)), username, req) +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) UnbindNamespace(ctx context.Context, namespaceID uint64, username 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)), username, 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) BindApp(ctx context.Context, appID uint64, username 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)), username, 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) UnbindApp(ctx context.Context, appID uint64, username 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)), username, 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) ListNamespaces(ctx context.Context) (*WayneRoleBindingResult, error) { @@ -101,12 +102,11 @@ func (s *WayneRoleBindingService) ListGroups(ctx context.Context, groupType *int return s.callRaw(ctx, http.MethodGet, internalPath, nil) } -func (s *WayneRoleBindingService) GetUserRoles(ctx context.Context, username string) (*WayneRoleBindingResult, error) { - username = strings.TrimSpace(username) - if username == "" { - return nil, ErrWayenEmailMissing +func (s *WayneRoleBindingService) GetUserRoles(ctx context.Context, userID uint64) (*WayneRoleBindingResult, error) { + if userID == 0 { + return nil, ErrWayneRoleBindingRequestFailed } - return s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/internal/users/%s/roles", url.PathEscape(username)), nil) + return s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/internal/users/%d/roles", userID), nil) } func (s *WayneRoleBindingService) NamespaceOperatorPermissions(ctx context.Context, namespaceID uint64, operatorEmail string) (*WayneRoleBindingResult, error) { @@ -128,6 +128,7 @@ func (s *WayneRoleBindingService) call(ctx context.Context, method, internalPath req.OperatorUserID = nil req.OperatorName = operatorEmail + req.UserID = nil body, err := json.Marshal(req) if err != nil { diff --git a/server/internal/service/wayne_role_binding_test.go b/server/internal/service/wayne_role_binding_test.go index c665046..782ce95 100644 --- a/server/internal/service/wayne_role_binding_test.go +++ b/server/internal/service/wayne_role_binding_test.go @@ -40,7 +40,9 @@ func TestWayneRoleBindingServiceBindNamespaceSignsAndOverridesOperator(t *testin operatorUserID := uint64(123) replace := false - result, err := svc.BindNamespace(context.Background(), 1, "eastsales@qiniu.com", WayneRoleBindingRequest{ + targetUserID := uint64(2001) + result, err := svc.BindNamespace(context.Background(), 1, targetUserID, "eastsales@qiniu.com", WayneRoleBindingRequest{ + UserID: &targetUserID, GroupIDs: []uint64{10, 11}, OperatorUserID: &operatorUserID, OperatorName: "attacker@example.com", @@ -54,7 +56,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/eastsales@qiniu.com/roles" { + if requestPath != "/api/v1/internal/namespaces/1/users/2001/roles" { t.Fatalf("requestPath = %q", requestPath) } if payload.OperatorName != "eastsales@qiniu.com" { @@ -63,6 +65,9 @@ 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.Replace == nil || *payload.Replace { t.Fatalf("Replace = %v, want false", payload.Replace) } @@ -77,23 +82,23 @@ func TestWayneRoleBindingServiceCallsAllDocumentedEndpoints(t *testing.T) { { name: "unbind namespace", call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { - return s.UnbindNamespace(context.Background(), 1, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{10}}) + return s.UnbindNamespace(context.Background(), 1, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{10}}) }, - want: "DELETE /api/v1/internal/namespaces/1/users/eastsales@qiniu.com/roles", + want: "DELETE /api/v1/internal/namespaces/1/users/2001/roles", }, { name: "bind app", call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { - return s.BindApp(context.Background(), 3, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) + return s.BindApp(context.Background(), 3, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) }, - want: "PUT /api/v1/internal/apps/3/users/eastsales@qiniu.com/roles", + want: "PUT /api/v1/internal/apps/3/users/2001/roles", }, { name: "unbind app", call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { - return s.UnbindApp(context.Background(), 3, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) + return s.UnbindApp(context.Background(), 3, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) }, - want: "DELETE /api/v1/internal/apps/3/users/eastsales@qiniu.com/roles", + want: "DELETE /api/v1/internal/apps/3/users/2001/roles", }, { name: "list namespaces", @@ -120,9 +125,9 @@ func TestWayneRoleBindingServiceCallsAllDocumentedEndpoints(t *testing.T) { { name: "get user roles", call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) { - return s.GetUserRoles(context.Background(), "eastsales@qiniu.com") + return s.GetUserRoles(context.Background(), 2001) }, - want: "GET /api/v1/internal/users/eastsales@qiniu.com/roles", + want: "GET /api/v1/internal/users/2001/roles", }, { name: "namespace operator permissions", @@ -226,7 +231,7 @@ func TestWayneRoleBindingServiceHTTPError(t *testing.T) { WayneServiceName: "xinfra", WayneServiceAPISecretKey: "service-secret", }) - result, err := svc.BindApp(context.Background(), 3, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) + result, err := svc.BindApp(context.Background(), 3, 2001, "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}}) if err == nil { t.Fatal("expected error") }