diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSuiteAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSuiteAppService.java index 3d1a64c1..22557485 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSuiteAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillSuiteAppService.java @@ -344,7 +344,7 @@ public class SkillSuiteAppService { .orElseThrow(() -> new DomainBadRequestException( "error.namespace.slug.notFound", request.namespace())); SkillSuiteDraftService.CreatedDraft created = draftService.create( - toCommand(namespace.getId(), request, userId, namespaceRoles), + toCommand(namespace.getId(), request, userId, namespaceRoles, platformRoles), context(userId, namespaceRoles, platformRoles, httpRequest)); return toResponse(namespace, created, userId, namespaceRoles, platformRoles); } @@ -361,7 +361,7 @@ public class SkillSuiteAppService { .orElseThrow(() -> new DomainBadRequestException( "error.namespace.slug.notFound", request.namespace())); SkillSuiteDraftService.CreatedDraft created = draftService.createVersion( - suiteId, toCommand(namespace.getId(), request, userId, namespaceRoles), + suiteId, toCommand(namespace.getId(), request, userId, namespaceRoles, platformRoles), context(userId, namespaceRoles, platformRoles, httpRequest)); return toResponse(namespace, created, userId, namespaceRoles, platformRoles); } @@ -379,7 +379,7 @@ public class SkillSuiteAppService { .orElseThrow(() -> new DomainBadRequestException( "error.namespace.slug.notFound", request.namespace())); SkillSuiteDraftService.CreatedDraft updated = draftService.updateDraft( - suiteId, versionId, toCommand(namespace.getId(), request, userId, namespaceRoles), + suiteId, versionId, toCommand(namespace.getId(), request, userId, namespaceRoles, platformRoles), context(userId, namespaceRoles, platformRoles, httpRequest)); return toResponse(namespace, updated, userId, namespaceRoles, platformRoles); } @@ -510,10 +510,11 @@ public class SkillSuiteAppService { private SkillSuiteMemberSelection resolve( SkillSuiteMemberRequest member, String userId, - Map namespaceRoles + Map namespaceRoles, + Set platformRoles ) { SkillQueryService.ResolvedVersionDTO resolved = skillQueryService.resolveVersionById( - member.skillVersionId(), userId, namespaceRoles); + member.skillVersionId(), userId, namespaceRoles, platformRoles); if (!Objects.equals(resolved.namespace(), member.namespace()) || !Objects.equals(resolved.slug(), member.slug()) || !Objects.equals(resolved.version(), member.version())) { @@ -528,10 +529,11 @@ public class SkillSuiteAppService { Long namespaceId, SkillSuiteCreateRequest request, String userId, - Map namespaceRoles + Map namespaceRoles, + Set platformRoles ) { List selections = request.members().stream() - .map(member -> resolve(member, userId, namespaceRoles)) + .map(member -> resolve(member, userId, namespaceRoles, platformRoles)) .toList(); Long entryVersionId = resolveEntryVersionId(request.entrySkill(), selections); return new CreateSkillSuiteDraftCommand( diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSuiteAppServiceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSuiteAppServiceTest.java index e05618ee..ec6f0f6e 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSuiteAppServiceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/service/SkillSuiteAppServiceTest.java @@ -273,18 +273,19 @@ class SkillSuiteAppServiceTest { } @Test - void create_rejectsWhenExactVersionIdDoesNotMatchTheSubmittedCoordinate() { + void create_rejectsCoordinateMismatchAfterResolvingWithPlatformRoles() { SkillSuiteMemberRequest member = new SkillSuiteMemberRequest( 101L, "global", "selected", "1.0.0"); SkillSuiteCreateRequest createRequest = new SkillSuiteCreateRequest( "global", "starter", "Starter", null, null, "1.0.0", SkillVisibility.PRIVATE, null, member, List.of(member)); given(namespaceRepository.findBySlug("global")).willReturn(java.util.Optional.of(namespace)); - given(skillQueryService.resolveVersionById(101L, "user-1", Map.of())) + given(skillQueryService.resolveVersionById( + 101L, "user-1", Map.of(), Set.of("SUPER_ADMIN"))) .willReturn(resolved(99L, 101L, "different", "1.0.0", "sha256:different")); assertThatThrownBy(() -> service.create( - createRequest, "user-1", Map.of(), Set.of(), request)) + createRequest, "user-1", Map.of(), Set.of("SUPER_ADMIN"), request)) .isInstanceOfSatisfying(DomainBadRequestException.class, exception -> assertThat(exception.messageCode()) .isEqualTo("error.suite.members.selectionMismatch")); diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java index 07f5e590..61565ae4 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillQueryService.java @@ -626,7 +626,8 @@ public class SkillQueryService { public ResolvedVersionDTO resolveVersionById( Long versionId, String currentUserId, - Map userNsRoles + Map userNsRoles, + Set platformRoles ) { SkillVersion version = skillVersionRepository.findById(versionId) .orElseThrow(() -> new DomainBadRequestException("error.skill.version.notFound", versionId)); @@ -635,7 +636,7 @@ public class SkillQueryService { Namespace namespace = namespaceRepository.findById(skill.getNamespaceId()) .orElseThrow(() -> new DomainBadRequestException( "error.namespace.id.notFound", skill.getNamespaceId())); - assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles); + assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles, platformRoles); assertInstallableVersion(version, version.getVersion()); String fingerprint = computeFingerprint(version); return new ResolvedVersionDTO( @@ -881,6 +882,15 @@ public class SkillQueryService { Skill skill, String currentUserId, Map userNsRoles) { + assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles, Set.of()); + } + + private void assertPublishedAccessible( + Namespace namespace, + Skill skill, + String currentUserId, + Map userNsRoles, + Set platformRoles) { if (namespace.getStatus() == NamespaceStatus.ARCHIVED && !isNamespaceMember(skill.getNamespaceId(), currentUserId, userNsRoles)) { throw new DomainForbiddenException("error.namespace.archived", namespace.getSlug()); } @@ -890,7 +900,7 @@ public class SkillQueryService { if (skill.isHidden() && !canManageRestrictedSkill(skill, currentUserId, userNsRoles)) { throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug()); } - if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles)) { + if (!visibilityChecker.canAccess(skill, currentUserId, userNsRoles, platformRoles)) { throw new DomainForbiddenException("error.skill.access.denied", skill.getSlug()); } } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java index 439f3262..3a8a8ed9 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/service/SkillQueryServiceTest.java @@ -765,7 +765,7 @@ class SkillQueryServiceTest { when(skillFileRepository.findByVersionId(220L)).thenReturn(List.of(file)); SkillQueryService.ResolvedVersionDTO result = service.resolveVersionById( - 220L, "current-owner", Map.of(1L, NamespaceRole.MEMBER)); + 220L, "current-owner", Map.of(1L, NamespaceRole.MEMBER), Set.of()); assertEquals(22L, result.skillId()); assertEquals(220L, result.versionId()); @@ -774,6 +774,32 @@ class SkillQueryServiceTest { verify(skillRepository, never()).findByNamespaceIdAndSlug(anyLong(), anyString()); } + @Test + void testResolveVersionById_ShouldAllowSuperAdminToSelectPrivatePublishedSkill() throws Exception { + Namespace namespace = new Namespace("team", "Team", "namespace-owner"); + setId(namespace, 1L); + Skill selected = new Skill(1L, "private-member", "skill-owner", SkillVisibility.PRIVATE); + setId(selected, 22L); + selected.setStatus(SkillStatus.ACTIVE); + selected.setLatestVersionId(220L); + SkillVersion selectedVersion = new SkillVersion(22L, "1.0.0", "skill-owner"); + setId(selectedVersion, 220L); + selectedVersion.setStatus(SkillVersionStatus.PUBLISHED); + selectedVersion.setDownloadReady(true); + SkillFile file = new SkillFile(220L, "SKILL.md", 10L, "text/markdown", "hash", "key"); + + when(skillVersionRepository.findById(220L)).thenReturn(Optional.of(selectedVersion)); + when(skillRepository.findById(22L)).thenReturn(Optional.of(selected)); + when(namespaceRepository.findById(1L)).thenReturn(Optional.of(namespace)); + when(skillFileRepository.findByVersionId(220L)).thenReturn(List.of(file)); + + SkillQueryService.ResolvedVersionDTO result = service.resolveVersionById( + 220L, "super-admin", Map.of(), Set.of("SUPER_ADMIN")); + + assertEquals(220L, result.versionId()); + assertEquals("private-member", result.slug()); + } + @Test void testResolveVersion_ShouldRejectDownloadUnavailableLatestVersion() throws Exception { String namespaceSlug = "global";