From d7d0790b289ecd104c3d9ca919a87415f41014de Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 18 May 2026 15:51:19 +0800 Subject: [PATCH] fix(auth): close API token scope filter gap on /api/cli/ routes ApiTokenAuthenticationFilter authenticates /api/cli/** Bearer tokens but ApiTokenScopeFilter.shouldNotFilter() previously skipped them. The result: API token requests on CLI routes were authenticated and authorization-policy-checked, but scope enforcement never ran. Tokens without skill:publish or skill:delete could call /publish, /publish/validate, and DELETE despite the policy table requiring those scopes. Add /api/cli/ to the scope filter's covered prefixes and a filter-level test that confirms a token missing skill:publish is rejected on the new validate endpoint. Update the existing CLI controller tests to grant the appropriate SCOPE_* authorities to their api_token principals so they continue to pass under enforced scopes. --- .../controller/cli/CliDryRunValidateTest.java | 4 ++- .../cli/CliSkillControllerTest.java | 4 ++- .../auth/token/ApiTokenScopeFilter.java | 3 +- .../auth/token/ApiTokenScopeFilterTest.java | 36 +++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliDryRunValidateTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliDryRunValidateTest.java index 0e10d343..f2224de0 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliDryRunValidateTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliDryRunValidateTest.java @@ -37,7 +37,9 @@ class CliDryRunValidateTest { PlatformPrincipal principal = new PlatformPrincipal( "user-1", "tester", "t@example.com", "", "api_token", Set.of("USER")); return new UsernamePasswordAuthenticationToken( - principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + principal, null, List.of( + new SimpleGrantedAuthority("ROLE_USER"), + new SimpleGrantedAuthority("SCOPE_skill:publish"))); } @Test diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliSkillControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliSkillControllerTest.java index 16a76839..d7edf81d 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliSkillControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/cli/CliSkillControllerTest.java @@ -103,7 +103,9 @@ class CliSkillControllerTest { PlatformPrincipal principal = new PlatformPrincipal( "user-1", "tester", "t@example.com", "", "api_token", Set.of("USER")); var auth = new UsernamePasswordAuthenticationToken( - principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER"))); + principal, null, List.of( + new SimpleGrantedAuthority("ROLE_USER"), + new SimpleGrantedAuthority("SCOPE_skill:delete"))); given(cliSkillAppService.deleteRemote( org.mockito.ArgumentMatchers.eq("global"), diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java index 8f69d5aa..97145f5d 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilter.java @@ -70,7 +70,8 @@ public class ApiTokenScopeFilter extends OncePerRequestFilter { protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); return path == null || (!path.startsWith("/api/v1/") - && !path.startsWith("/api/web/")); + && !path.startsWith("/api/web/") + && !path.startsWith("/api/cli/")); } private boolean isApiTokenAuthentication(Authentication authentication) { diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java index 9156eba8..788e0291 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/token/ApiTokenScopeFilterTest.java @@ -134,4 +134,40 @@ class ApiTokenScopeFilterTest { assertTrue(response.getErrorMessage().contains("Missing API token scope: skill:publish")); verify(chain, never()).doFilter(request, response); } + + @Test + void shouldDenyApiCliRequestsWithoutRequiredScope() throws Exception { + AccessDeniedHandler handler = (request, response, accessDeniedException) -> { + response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); + }; + ApiTokenScopeFilter filter = new ApiTokenScopeFilter(scopeService, handler); + + PlatformPrincipal principal = new PlatformPrincipal( + "user-4", + "Dave", + "dave@example.com", + "", + "api_token", + Set.of("USER") + ); + var authentication = new UsernamePasswordAuthenticationToken( + principal, + null, + List.of( + new SimpleGrantedAuthority("ROLE_USER"), + new SimpleGrantedAuthority("SCOPE_skill:read") + ) + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/cli/v1/skills/global/publish/validate"); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain chain = mock(FilterChain.class); + + filter.doFilter(request, response, chain); + + assertEquals(HttpServletResponse.SC_FORBIDDEN, response.getStatus()); + assertTrue(response.getErrorMessage().contains("Missing API token scope: skill:publish")); + verify(chain, never()).doFilter(request, response); + } }