From ad8bb9c6fd94b9d762ca7ac8ce60256d07fd310b Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <15071461069@163.com> Date: Fri, 13 Mar 2026 11:12:18 +0800 Subject: [PATCH] fix(security): whitelist only public skill GET routes - require authentication for skill star and rating GET endpoints before the public skill-read rules - keep documented public skill detail, version, download, resolve, and tag listing endpoints readable anonymously - add regression coverage for anonymous star and rating access denial plus public tag listing --- .../controller/SkillRatingControllerTest.java | 6 +++ .../controller/SkillStarControllerTest.java | 6 +++ .../controller/SkillTagControllerTest.java | 48 +++++++++++++++++++ .../skillhub/auth/config/SecurityConfig.java | 18 ++++++- 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillTagControllerTest.java diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java index 8bc0dceb..7043ac04 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java @@ -107,4 +107,10 @@ class SkillRatingControllerTest { .content("{\"score\": 4}")) .andExpect(status().isUnauthorized()); } + + @Test + void get_user_rating_unauthenticated_returns_401() throws Exception { + mockMvc.perform(get("/api/v1/skills/10/rating")) + .andExpect(status().isUnauthorized()); + } } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java index de0b5f92..2912fa7f 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java @@ -126,4 +126,10 @@ class SkillStarControllerTest { .andExpect(jsonPath("$.timestamp").isNotEmpty()) .andExpect(jsonPath("$.requestId").isNotEmpty()); } + + @Test + void check_starred_unauthenticated_returns_401() throws Exception { + mockMvc.perform(get("/api/v1/skills/10/star")) + .andExpect(status().isUnauthorized()); + } } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillTagControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillTagControllerTest.java new file mode 100644 index 00000000..2daffdcd --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillTagControllerTest.java @@ -0,0 +1,48 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +import com.iflytek.skillhub.domain.skill.SkillTag; +import com.iflytek.skillhub.domain.skill.service.SkillTagService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class SkillTagControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SkillTagService skillTagService; + + @MockBean + private NamespaceMemberRepository namespaceMemberRepository; + + @Test + void list_tags_is_public() throws Exception { + when(skillTagService.listTags(eq("team"), eq("demo"))) + .thenReturn(List.of(new SkillTag(1L, "latest", 2L, "user-1"))); + + mockMvc.perform(get("/api/v1/skills/team/demo/tags")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data[0].tagName").value("latest")) + .andExpect(jsonPath("$.timestamp").isNotEmpty()) + .andExpect(jsonPath("$.requestId").isNotEmpty()); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java index a72455e7..828a6755 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java @@ -79,7 +79,23 @@ public class SecurityConfig { "/api/compat/v1/search", "/api/compat/v1/resolve/**" ).permitAll() - .requestMatchers(HttpMethod.GET, "/api/v1/skills", "/api/v1/skills/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v1/skills/*/star", "/api/v1/skills/*/rating").authenticated() + .requestMatchers( + HttpMethod.GET, + "/api/v1/skills", + "/api/v1/skills/*/*", + "/api/v1/skills/*/*/versions", + "/api/v1/skills/*/*/versions/*", + "/api/v1/skills/*/*/versions/*/files", + "/api/v1/skills/*/*/versions/*/file", + "/api/v1/skills/*/*/resolve", + "/api/v1/skills/*/*/download", + "/api/v1/skills/*/*/versions/*/download", + "/api/v1/skills/*/*/tags", + "/api/v1/skills/*/*/tags/*/files", + "/api/v1/skills/*/*/tags/*/file", + "/api/v1/skills/*/*/tags/*/download" + ).permitAll() .requestMatchers(HttpMethod.GET, "/api/v1/namespaces", "/api/v1/namespaces/*").permitAll() .requestMatchers("/api/v1/admin/**").hasAnyRole("SUPER_ADMIN", "SKILL_ADMIN", "USER_ADMIN", "AUDITOR") .anyRequest().authenticated()