From e020ffed4932afd41e3f7dab733468e558f69237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 10 Jul 2026 19:47:53 +0800 Subject: [PATCH] Add files via upload --- internal/security/rbac_middleware.go | 73 ++++++++++++++++------- internal/security/rbac_middleware_test.go | 25 ++++++++ 2 files changed, 77 insertions(+), 21 deletions(-) diff --git a/internal/security/rbac_middleware.go b/internal/security/rbac_middleware.go index e21d08ba..28c289ce 100644 --- a/internal/security/rbac_middleware.go +++ b/internal/security/rbac_middleware.go @@ -29,31 +29,62 @@ func RBACMiddlewareWithDenyHook(db *database.DB, denyHook RBACDenyHook) gin.Hand }) return } - if SessionHasPermission(c, permission) { - // Bind the scope of the permission authorizing this request. Scope is - // permission-specific; using the user's broadest role scope here would - // let an unrelated global read role widen a write permission. - session, _ := CurrentSession(c) - session.Scope = session.ScopeFor(permission) - c.Set(ContextSessionKey, session) - c.Set(ContextUserScopeKey, session.Scope) - if db != nil && !resourceAllowed(c, db) { - if denyHook != nil { - denyHook(c, "resource_denied", permission) - } - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"}) - return + permission, allowed := sessionHasRoutePermission(c, c.Request.Method, c.FullPath()) + if !allowed { + if denyHook != nil { + denyHook(c, "permission_denied", permission) } - c.Next() + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ + "error": "权限不足", + "permission": permission, + }) return } - if denyHook != nil { - denyHook(c, "permission_denied", permission) + // Bind the scope of the permission authorizing this request. Scope is + // permission-specific; using the user's broadest role scope here would + // let an unrelated global read role widen a write permission. + session, _ := CurrentSession(c) + session.Scope = session.ScopeFor(permission) + c.Set(ContextSessionKey, session) + c.Set(ContextUserScopeKey, session.Scope) + if db != nil && !resourceAllowed(c, db) { + if denyHook != nil { + denyHook(c, "resource_denied", permission) + } + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"}) + return } - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ - "error": "权限不足", - "permission": permission, - }) + c.Next() + } +} + +func sessionHasRoutePermission(c *gin.Context, method, fullPath string) (string, bool) { + path := strings.TrimPrefix(fullPath, "/api") + if alts := permissionAlternativesForRequest(method, path); len(alts) > 0 { + for _, permission := range alts { + if SessionHasPermission(c, permission) { + return permission, true + } + } + return alts[0], false + } + permission := permissionForRequest(method, fullPath) + if permission == "" { + return "", false + } + return permission, SessionHasPermission(c, permission) +} + +func permissionAlternativesForRequest(method, path string) []string { + if method != http.MethodGet && method != http.MethodHead { + return nil + } + switch { + case strings.HasPrefix(path, "/config/tools"): + // MCP 管理页只需 mcp:read;系统设置页仍可用 config:read 访问同一接口。 + return []string{"mcp:read", "config:read"} + default: + return nil } } diff --git a/internal/security/rbac_middleware_test.go b/internal/security/rbac_middleware_test.go index fec7ccb1..1e89b28c 100644 --- a/internal/security/rbac_middleware_test.go +++ b/internal/security/rbac_middleware_test.go @@ -128,6 +128,31 @@ func TestMCPInvocationPermissionIsSeparateFromMCPAdministration(t *testing.T) { } } +func TestConfigToolsReadAllowsMCPReadWithoutConfigRead(t *testing.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + router.Use(func(c *gin.Context) { + c.Set(ContextSessionKey, Session{ + UserID: "viewer", + Username: "viewer", + Permissions: map[string]bool{"mcp:read": true}, + Scope: database.RBACScopeAssigned, + }) + c.Next() + }) + router.Use(RBACMiddleware(nil)) + router.GET("/api/config/tools", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"tools": []any{}}) + }) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/config/tools", nil) + router.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want %d: %s", w.Code, http.StatusOK, w.Body.String()) + } +} + func TestWorkflowRunPermissionIsSeparateFromDefinitionManagement(t *testing.T) { if got := permissionForRequest(http.MethodPost, "/api/workflows/runs/run-1/resume"); got != "workflow:execute" { t.Fatalf("resume permission = %q, want workflow:execute", got)