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
This commit is contained in:
yun-zhi-ztl 2026-03-13 11:12:18 +08:00
parent ec8f7ec838
commit ad8bb9c6fd
4 changed files with 77 additions and 1 deletions

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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()