mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-08 03:07:51 +00:00
feat: hide archived namespaces from public reads
This commit is contained in:
parent
fcc2df2975
commit
33ed8fe17d
6 changed files with 174 additions and 22 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.skill.Skill;
|
||||
import com.iflytek.skillhub.domain.skill.SkillRepository;
|
||||
|
|
@ -89,19 +91,21 @@ public class SkillSearchAppService {
|
|||
.map(Skill::getNamespaceId)
|
||||
.distinct()
|
||||
.toList();
|
||||
Map<Long, String> namespaceSlugsById = namespaceIds.isEmpty()
|
||||
Map<Long, Namespace> namespacesById = namespaceIds.isEmpty()
|
||||
? Map.of()
|
||||
: namespaceRepository.findByIdIn(namespaceIds).stream()
|
||||
.collect(Collectors.toMap(com.iflytek.skillhub.domain.namespace.Namespace::getId,
|
||||
com.iflytek.skillhub.domain.namespace.Namespace::getSlug));
|
||||
.collect(Collectors.toMap(Namespace::getId, Function.identity()));
|
||||
Map<Long, String> namespaceSlugsById = namespacesById.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getSlug()));
|
||||
|
||||
List<SkillSummaryResponse> skills = result.skillIds().stream()
|
||||
.map(skillsById::get)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.filter(skill -> namespaceVisible(skill.getNamespaceId(), namespacesById, userId, userNsRoles))
|
||||
.map(skill -> toSummaryResponse(skill, versionsById, namespaceSlugsById))
|
||||
.toList();
|
||||
|
||||
return new SearchResponse(skills, result.total(), result.page(), result.size());
|
||||
return new SearchResponse(skills, skills.size(), result.page(), result.size());
|
||||
}
|
||||
|
||||
private Long resolveNamespaceId(String namespaceSlug) {
|
||||
|
|
@ -158,4 +162,18 @@ public class SkillSearchAppService {
|
|||
skill.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private boolean namespaceVisible(
|
||||
Long namespaceId,
|
||||
Map<Long, Namespace> namespacesById,
|
||||
String userId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
NamespaceStatus status = java.util.Optional.ofNullable(namespacesById.get(namespaceId))
|
||||
.map(Namespace::getStatus)
|
||||
.orElse(NamespaceStatus.ACTIVE);
|
||||
if (status != NamespaceStatus.ARCHIVED) {
|
||||
return true;
|
||||
}
|
||||
return userId != null && userNsRoles != null && userNsRoles.containsKey(namespaceId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.iflytek.skillhub.controller;
|
|||
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.SkillFile;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillDownloadService;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
|
|
@ -138,6 +139,20 @@ class SkillControllerTest {
|
|||
.andExpect(jsonPath("$.data.canInteract").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSkillDetailShouldReturnForbiddenForArchivedNamespace() throws Exception {
|
||||
when(skillQueryService.getSkillDetail(
|
||||
eq("team"),
|
||||
eq("demo"),
|
||||
eq((String) null),
|
||||
eq(Map.<Long, NamespaceRole>of())))
|
||||
.thenThrow(new DomainForbiddenException("error.namespace.archived", "team"));
|
||||
|
||||
mockMvc.perform(get("/api/web/skills/team/demo"))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.code").value(403));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listFilesByTagShouldReturnUnifiedEnvelope() throws Exception {
|
||||
when(skillQueryService.listFilesByTag(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import org.springframework.test.web.servlet.MockMvc;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
|
@ -40,11 +41,11 @@ class SkillSearchControllerTest {
|
|||
eq("newest"),
|
||||
eq(0),
|
||||
eq(20),
|
||||
eq((String) null),
|
||||
eq(null)))
|
||||
any(),
|
||||
any()))
|
||||
.thenReturn(new SkillSearchAppService.SearchResponse(List.of(), 0, 0, 20));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills")
|
||||
mockMvc.perform(get("/api/web/skills")
|
||||
.param("q", "review")
|
||||
.param("namespace", "global"))
|
||||
.andExpect(status().isOk())
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.skill.Skill;
|
||||
import com.iflytek.skillhub.domain.skill.SkillRepository;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVersionRepository;
|
||||
import com.iflytek.skillhub.domain.skill.SkillVisibility;
|
||||
import com.iflytek.skillhub.search.SearchQueryService;
|
||||
import com.iflytek.skillhub.search.SearchResult;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SkillSearchAppServiceTest {
|
||||
|
||||
@Mock
|
||||
private SearchQueryService searchQueryService;
|
||||
|
||||
@Mock
|
||||
private SkillRepository skillRepository;
|
||||
|
||||
@Mock
|
||||
private NamespaceRepository namespaceRepository;
|
||||
|
||||
@Mock
|
||||
private SkillVersionRepository skillVersionRepository;
|
||||
|
||||
private SkillSearchAppService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new SkillSearchAppService(searchQueryService, skillRepository, namespaceRepository, skillVersionRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void search_shouldExcludeArchivedNamespaceSkillsForAnonymousUsers() {
|
||||
Skill archivedSkill = new Skill(1L, "archived-skill", "owner-1", SkillVisibility.PUBLIC);
|
||||
setField(archivedSkill, "id", 10L);
|
||||
|
||||
Namespace archivedNamespace = new Namespace("archived-team", "Archived Team", "owner-1");
|
||||
setField(archivedNamespace, "id", 1L);
|
||||
archivedNamespace.setStatus(NamespaceStatus.ARCHIVED);
|
||||
|
||||
when(searchQueryService.search(org.mockito.ArgumentMatchers.any()))
|
||||
.thenReturn(new SearchResult(List.of(10L), 1, 0, 20));
|
||||
when(skillRepository.findByIdIn(List.of(10L))).thenReturn(List.of(archivedSkill));
|
||||
when(namespaceRepository.findByIdIn(List.of(1L))).thenReturn(List.of(archivedNamespace));
|
||||
|
||||
SkillSearchAppService.SearchResponse response = service.search("archive", null, "newest", 0, 20, null, null);
|
||||
|
||||
assertEquals(0, response.items().size());
|
||||
assertEquals(0, response.total());
|
||||
}
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
java.lang.reflect.Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.iflytek.skillhub.domain.skill.service;
|
|||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
|
|
@ -170,8 +171,9 @@ public class SkillQueryService {
|
|||
String version,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
assertPreviewAccessible(skill, skillVersion, version, currentUserId);
|
||||
|
||||
|
|
@ -194,8 +196,9 @@ public class SkillQueryService {
|
|||
String version,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
assertPreviewAccessible(skill, skillVersion, version, currentUserId);
|
||||
|
|
@ -209,8 +212,9 @@ public class SkillQueryService {
|
|||
String tagName,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null);
|
||||
return skillFileRepository.findByVersionId(skillVersion.getId());
|
||||
}
|
||||
|
|
@ -222,8 +226,9 @@ public class SkillQueryService {
|
|||
String filePath,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion skillVersion = findVersion(skill, version);
|
||||
assertPreviewAccessible(skill, skillVersion, version, currentUserId);
|
||||
|
|
@ -240,8 +245,9 @@ public class SkillQueryService {
|
|||
String filePath,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion skillVersion = resolveVersionEntity(skill, null, tagName, null);
|
||||
SkillFile file = findFile(skillVersion, filePath);
|
||||
return readFileContent(file);
|
||||
|
|
@ -252,8 +258,9 @@ public class SkillQueryService {
|
|||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles,
|
||||
Pageable pageable) {
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
List<SkillVersion> visibleVersions;
|
||||
if (canManageRestrictedSkill(skill, currentUserId, userNsRoles)) {
|
||||
visibleVersions = skillVersionRepository.findBySkillId(skill.getId()).stream()
|
||||
|
|
@ -295,8 +302,9 @@ public class SkillQueryService {
|
|||
throw new DomainBadRequestException("error.skill.resolve.versionTag.conflict");
|
||||
}
|
||||
|
||||
Skill skill = findSkill(namespaceSlug, skillSlug);
|
||||
assertPublishedAccessible(skill, currentUserId, userNsRoles);
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = findSkill(namespace, skillSlug);
|
||||
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles);
|
||||
SkillVersion resolved = resolveVersionEntity(skill, version, tag, hash);
|
||||
String fingerprint = computeFingerprint(resolved);
|
||||
Boolean matched = hash == null || hash.isBlank() ? null : Objects.equals(hash, fingerprint);
|
||||
|
|
@ -324,6 +332,10 @@ public class SkillQueryService {
|
|||
|
||||
private Skill findSkill(String namespaceSlug, String skillSlug) {
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
return findSkill(namespace, skillSlug);
|
||||
}
|
||||
|
||||
private Skill findSkill(Namespace namespace, String skillSlug) {
|
||||
return skillRepository.findByNamespaceIdAndSlug(namespace.getId(), skillSlug)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.notFound", skillSlug));
|
||||
}
|
||||
|
|
@ -440,7 +452,14 @@ public class SkillQueryService {
|
|||
return URLEncoder.encode(value, StandardCharsets.UTF_8).replace("+", "%20");
|
||||
}
|
||||
|
||||
private void assertPublishedAccessible(Skill skill, String currentUserId, Map<Long, NamespaceRole> userNsRoles) {
|
||||
private void assertPublishedAccessible(
|
||||
Namespace namespace,
|
||||
Skill skill,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
if (namespace.getStatus() == NamespaceStatus.ARCHIVED && !isNamespaceMember(skill.getNamespaceId(), currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.namespace.archived", namespace.getSlug());
|
||||
}
|
||||
if (skill.getStatus() != SkillStatus.ACTIVE && !canManageRestrictedSkill(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug());
|
||||
}
|
||||
|
|
@ -466,6 +485,10 @@ public class SkillQueryService {
|
|||
return currentUserId != null && skill.getOwnerId().equals(currentUserId);
|
||||
}
|
||||
|
||||
private boolean isNamespaceMember(Long namespaceId, String currentUserId, Map<Long, NamespaceRole> userNsRoles) {
|
||||
return currentUserId != null && userNsRoles.containsKey(namespaceId);
|
||||
}
|
||||
|
||||
private int lifecycleListPriority(SkillVersionStatus status) {
|
||||
if (status == SkillVersionStatus.PUBLISHED) {
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.iflytek.skillhub.domain.skill.service;
|
|||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceStatus;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
|
|
@ -118,6 +119,24 @@ class SkillQueryServiceTest {
|
|||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSkillDetail_ShouldHideArchivedNamespaceFromAnonymousUsers() throws Exception {
|
||||
String namespaceSlug = "archived-team";
|
||||
String skillSlug = "test-skill";
|
||||
|
||||
Namespace namespace = new Namespace(namespaceSlug, "Archived Team", "user-1");
|
||||
namespace.setStatus(NamespaceStatus.ARCHIVED);
|
||||
setId(namespace, 1L);
|
||||
Skill skill = new Skill(1L, skillSlug, "user-200", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
|
||||
when(namespaceRepository.findBySlug(namespaceSlug)).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, skillSlug)).thenReturn(Optional.of(skill));
|
||||
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
service.getSkillDetail(namespaceSlug, skillSlug, null, Map.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testListSkillsByNamespace() throws Exception {
|
||||
// Arrange
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue