mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
Allow anonymous downloads for global public skills
This commit is contained in:
parent
cc5fc7586f
commit
72054dee0f
4 changed files with 104 additions and 21 deletions
|
|
@ -10,6 +10,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||
import com.iflytek.skillhub.TestRedisConfig;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillDownloadService;
|
||||
import com.iflytek.skillhub.domain.skill.service.SkillQueryService;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
|
@ -99,13 +100,29 @@ class SkillControllerDownloadTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void downloadVersion_requiresAuthentication() throws Exception {
|
||||
void downloadVersion_allowsAnonymousForGlobalSkill() throws Exception {
|
||||
given(skillDownloadService.downloadVersion("global", "demo-skill", "1.0.0", null, java.util.Map.of()))
|
||||
.willReturn(new SkillDownloadService.DownloadResult(
|
||||
new ByteArrayInputStream("zip".getBytes()),
|
||||
"demo-skill-1.0.0.zip",
|
||||
3L,
|
||||
"application/zip",
|
||||
null
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills/global/demo-skill/versions/1.0.0/download")
|
||||
.with(csrf()))
|
||||
.andDo(result -> {
|
||||
System.out.println("Status: " + result.getResponse().getStatus());
|
||||
System.out.println("Body: " + result.getResponse().getContentAsString());
|
||||
})
|
||||
.andExpect(status().isUnauthorized());
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("Content-Disposition", "attachment; filename=\"demo-skill-1.0.0.zip\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadVersion_forbidsAnonymousWhenServiceRejectsSkill() throws Exception {
|
||||
given(skillDownloadService.downloadVersion("team-ai", "demo-skill", "1.0.0", null, java.util.Map.of()))
|
||||
.willThrow(new DomainForbiddenException("error.skill.access.denied", "demo-skill"));
|
||||
|
||||
mockMvc.perform(get("/api/v1/skills/team-ai/demo-skill/versions/1.0.0/download")
|
||||
.with(csrf()))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,20 +135,26 @@ public class SecurityConfig {
|
|||
"/api/v1/skills/*/*",
|
||||
"/api/v1/skills/*/*/versions",
|
||||
"/api/v1/skills/*/*/versions/*",
|
||||
"/api/v1/skills/*/*/download",
|
||||
"/api/v1/skills/*/*/versions/*/download",
|
||||
"/api/v1/skills/*/*/versions/*/files",
|
||||
"/api/v1/skills/*/*/versions/*/file",
|
||||
"/api/v1/skills/*/*/resolve",
|
||||
"/api/v1/skills/*/*/tags",
|
||||
"/api/v1/skills/*/*/tags/*/download",
|
||||
"/api/v1/skills/*/*/tags/*/files",
|
||||
"/api/v1/skills/*/*/tags/*/file",
|
||||
"/api/web/skills",
|
||||
"/api/web/skills/*/*",
|
||||
"/api/web/skills/*/*/versions",
|
||||
"/api/web/skills/*/*/versions/*",
|
||||
"/api/web/skills/*/*/download",
|
||||
"/api/web/skills/*/*/versions/*/download",
|
||||
"/api/web/skills/*/*/versions/*/files",
|
||||
"/api/web/skills/*/*/versions/*/file",
|
||||
"/api/web/skills/*/*/resolve",
|
||||
"/api/web/skills/*/*/tags",
|
||||
"/api/web/skills/*/*/tags/*/download",
|
||||
"/api/web/skills/*/*/tags/*/files",
|
||||
"/api/web/skills/*/*/tags/*/file"
|
||||
).permitAll()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.iflytek.skillhub.domain.event.SkillDownloadedEvent;
|
|||
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.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
|
|
@ -72,11 +73,7 @@ public class SkillDownloadService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
assertCanDownload(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
if (skill.getLatestVersionId() == null) {
|
||||
throw new DomainBadRequestException("error.skill.version.latest.unavailable", skillSlug);
|
||||
|
|
@ -97,11 +94,7 @@ public class SkillDownloadService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
assertCanDownload(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillVersion version = skillVersionRepository.findBySkillIdAndVersion(skill.getId(), versionStr)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", versionStr));
|
||||
|
|
@ -118,11 +111,7 @@ public class SkillDownloadService {
|
|||
|
||||
Namespace namespace = findNamespace(namespaceSlug);
|
||||
Skill skill = resolveVisibleSkill(namespace.getId(), skillSlug, currentUserId);
|
||||
|
||||
// Visibility check
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skillSlug);
|
||||
}
|
||||
assertCanDownload(namespace, skill, currentUserId, userNsRoles);
|
||||
|
||||
SkillTag tag = skillTagRepository.findBySkillIdAndTagName(skill.getId(), tagName)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.skill.tag.notFound", tagName));
|
||||
|
|
@ -217,6 +206,23 @@ public class SkillDownloadService {
|
|||
.orElseThrow(() -> new DomainBadRequestException("error.namespace.slug.notFound", slug));
|
||||
}
|
||||
|
||||
private void assertCanDownload(Namespace namespace,
|
||||
Skill skill,
|
||||
String currentUserId,
|
||||
Map<Long, NamespaceRole> userNsRoles) {
|
||||
if (currentUserId == null && !isAnonymousDownloadAllowed(namespace, skill)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug());
|
||||
}
|
||||
if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) {
|
||||
throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAnonymousDownloadAllowed(Namespace namespace, Skill skill) {
|
||||
return namespace.getType() == NamespaceType.GLOBAL
|
||||
&& skill.getVisibility() == SkillVisibility.PUBLIC;
|
||||
}
|
||||
|
||||
private Skill resolveVisibleSkill(Long namespaceId, String slug, String currentUserId) {
|
||||
return skillSlugResolutionService.resolve(
|
||||
namespaceId,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import com.iflytek.skillhub.domain.event.SkillDownloadedEvent;
|
|||
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.NamespaceType;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.skill.*;
|
||||
import com.iflytek.skillhub.storage.ObjectMetadata;
|
||||
import com.iflytek.skillhub.storage.ObjectStorageService;
|
||||
|
|
@ -267,6 +269,58 @@ class SkillDownloadServiceTest {
|
|||
verify(eventPublisher).publishEvent(any(SkillDownloadedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadVersion_AllowsAnonymousForGlobalPublicSkill() throws Exception {
|
||||
Namespace namespace = new Namespace("global", "Global", "system");
|
||||
setId(namespace, 1L);
|
||||
namespace.setType(NamespaceType.GLOBAL);
|
||||
|
||||
Skill skill = new Skill(1L, "demo-skill", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setDisplayName("Demo Skill");
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
SkillVersion version = new SkillVersion(1L, "1.0.0", "owner-1");
|
||||
setId(version, 10L);
|
||||
version.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
|
||||
when(namespaceRepository.findBySlug("global")).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(1L, "demo-skill")).thenReturn(List.of(skill));
|
||||
when(visibilityChecker.canAccess(skill, null, Map.of())).thenReturn(true);
|
||||
when(skillVersionRepository.findBySkillIdAndVersion(1L, "1.0.0")).thenReturn(Optional.of(version));
|
||||
when(objectStorageService.exists("packages/1/10/bundle.zip")).thenReturn(false);
|
||||
when(skillFileRepository.findByVersionId(10L)).thenReturn(List.of(
|
||||
new SkillFile(10L, "SKILL.md", 4L, "text/markdown", "hash", "skills/1/10/SKILL.md")));
|
||||
when(objectStorageService.exists("skills/1/10/SKILL.md")).thenReturn(true);
|
||||
when(objectStorageService.getObject("skills/1/10/SKILL.md")).thenReturn(new ByteArrayInputStream("test".getBytes()));
|
||||
|
||||
SkillDownloadService.DownloadResult result = service.downloadVersion("global", "demo-skill", "1.0.0", null, Map.of());
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("Demo Skill-1.0.0.zip", result.filename());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDownloadVersion_RejectsAnonymousForTeamNamespacePublicSkill() throws Exception {
|
||||
Namespace namespace = new Namespace("team-ai", "Team AI", "owner-1");
|
||||
setId(namespace, 2L);
|
||||
namespace.setType(NamespaceType.TEAM);
|
||||
|
||||
Skill skill = new Skill(2L, "demo-skill", "owner-1", SkillVisibility.PUBLIC);
|
||||
setId(skill, 1L);
|
||||
skill.setStatus(SkillStatus.ACTIVE);
|
||||
skill.setLatestVersionId(10L);
|
||||
|
||||
when(namespaceRepository.findBySlug("team-ai")).thenReturn(Optional.of(namespace));
|
||||
when(skillRepository.findByNamespaceIdAndSlug(2L, "demo-skill")).thenReturn(List.of(skill));
|
||||
|
||||
assertThrows(DomainForbiddenException.class, () ->
|
||||
service.downloadVersion("team-ai", "demo-skill", "1.0.0", null, Map.of()));
|
||||
|
||||
verify(visibilityChecker, never()).canAccess(any(), any(), anyMap());
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) throws Exception {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue