fix(suite): align super admin member selection

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-09 12:38:49 +08:00
parent a4b35b236a
commit 2e0cd691aa
4 changed files with 53 additions and 14 deletions

View file

@ -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<Long, NamespaceRole> namespaceRoles
Map<Long, NamespaceRole> namespaceRoles,
Set<String> 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<Long, NamespaceRole> namespaceRoles
Map<Long, NamespaceRole> namespaceRoles,
Set<String> platformRoles
) {
List<SkillSuiteMemberSelection> 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(

View file

@ -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"));

View file

@ -626,7 +626,8 @@ public class SkillQueryService {
public ResolvedVersionDTO resolveVersionById(
Long versionId,
String currentUserId,
Map<Long, NamespaceRole> userNsRoles
Map<Long, NamespaceRole> userNsRoles,
Set<String> 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<Long, NamespaceRole> userNsRoles) {
assertPublishedAccessible(namespace, skill, currentUserId, userNsRoles, Set.of());
}
private void assertPublishedAccessible(
Namespace namespace,
Skill skill,
String currentUserId,
Map<Long, NamespaceRole> userNsRoles,
Set<String> 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());
}
}

View file

@ -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";