mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
fix(namespace): let super admin view namespaces
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
76d95b615e
commit
c99df875f2
5 changed files with 157 additions and 20 deletions
|
|
@ -61,23 +61,28 @@ public class NamespaceController extends BaseApiController {
|
|||
@GetMapping("/namespaces")
|
||||
public ApiResponse<PageResponse<NamespaceResponse>> listNamespaces(
|
||||
Pageable pageable,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
return ok("response.success.read", namespacePortalQueryAppService.listNamespaces(pageable, userNsRoles));
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
return ok("response.success.read",
|
||||
namespacePortalQueryAppService.listNamespaces(pageable, userNsRoles, platformRoles(principal)));
|
||||
}
|
||||
|
||||
@GetMapping("/me/namespaces")
|
||||
public ApiResponse<List<MyNamespaceResponse>> listMyNamespaces(
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
return ok("response.success.read", namespacePortalQueryAppService.listMyNamespaces(userNsRoles));
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
return ok("response.success.read",
|
||||
namespacePortalQueryAppService.listMyNamespaces(userNsRoles, platformRoles(principal)));
|
||||
}
|
||||
|
||||
@GetMapping("/namespaces/{slug}")
|
||||
public ApiResponse<NamespaceResponse> getNamespace(@PathVariable String slug,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
return ok("response.success.read",
|
||||
namespacePortalQueryAppService.getNamespace(slug, userId, userNsRoles));
|
||||
namespacePortalQueryAppService.getNamespace(slug, userId, userNsRoles, platformRoles(principal)));
|
||||
}
|
||||
|
||||
@PostMapping("/namespaces")
|
||||
|
|
@ -158,11 +163,14 @@ public class NamespaceController extends BaseApiController {
|
|||
Pageable pageable,
|
||||
@RequestAttribute("userId") String userId,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
Set<String> platformRoles = principal != null && principal.platformRoles() != null
|
||||
return ok("response.success.read",
|
||||
namespacePortalQueryAppService.listMembers(slug, pageable, userId, platformRoles(principal)));
|
||||
}
|
||||
|
||||
private Set<String> platformRoles(PlatformPrincipal principal) {
|
||||
return principal != null && principal.platformRoles() != null
|
||||
? principal.platformRoles()
|
||||
: Set.of();
|
||||
return ok("response.success.read",
|
||||
namespacePortalQueryAppService.listMembers(slug, pageable, userId, platformRoles));
|
||||
}
|
||||
|
||||
@GetMapping("/namespaces/{slug}/member-candidates")
|
||||
|
|
|
|||
|
|
@ -55,7 +55,14 @@ public class NamespacePortalQueryAppService {
|
|||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<NamespaceResponse> listNamespaces(Pageable pageable, Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
public PageResponse<NamespaceResponse> listNamespaces(Pageable pageable,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
if (isSuperAdmin(platformRoles)) {
|
||||
return PageResponse.from(namespaceRepository.findByStatus(NamespaceStatus.ACTIVE, pageable)
|
||||
.map(NamespaceResponse::from));
|
||||
}
|
||||
|
||||
Map<Long, NamespaceRole> namespaceRoles = userNamespaceRoles != null ? userNamespaceRoles : Map.of();
|
||||
if (namespaceRoles.isEmpty()) {
|
||||
Page<NamespaceResponse> empty = new PageImpl<>(
|
||||
|
|
@ -83,24 +90,34 @@ public class NamespacePortalQueryAppService {
|
|||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<MyNamespaceResponse> listMyNamespaces(Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
public List<MyNamespaceResponse> listMyNamespaces(Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
Map<Long, NamespaceRole> namespaceRoles = userNamespaceRoles != null ? userNamespaceRoles : Map.of();
|
||||
if (isSuperAdmin(platformRoles)) {
|
||||
return namespaceRepository.findAll().stream()
|
||||
.sorted(Comparator.comparing(Namespace::getSlug))
|
||||
.map(namespace -> toMyNamespaceResponse(namespace, namespaceRoles.get(namespace.getId())))
|
||||
.toList();
|
||||
}
|
||||
if (namespaceRoles.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return namespaceRepository.findByIdIn(namespaceRoles.keySet().stream().toList()).stream()
|
||||
.sorted(Comparator.comparing(Namespace::getSlug))
|
||||
.map(namespace -> MyNamespaceResponse.from(
|
||||
namespace,
|
||||
namespaceRoles.get(namespace.getId()),
|
||||
namespaceAccessPolicy,
|
||||
namespaceService.canDelete(namespace, namespaceRoles.get(namespace.getId()))))
|
||||
.map(namespace -> toMyNamespaceResponse(namespace, namespaceRoles.get(namespace.getId())))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public NamespaceResponse getNamespace(String slug, String userId, Map<Long, NamespaceRole> userNamespaceRoles) {
|
||||
public NamespaceResponse getNamespace(String slug,
|
||||
String userId,
|
||||
Map<Long, NamespaceRole> userNamespaceRoles,
|
||||
Set<String> platformRoles) {
|
||||
if (isSuperAdmin(platformRoles)) {
|
||||
return NamespaceResponse.from(namespaceService.getNamespaceBySlug(slug));
|
||||
}
|
||||
|
||||
Map<Long, NamespaceRole> namespaceRoles = userNamespaceRoles != null ? userNamespaceRoles : Map.of();
|
||||
Namespace namespace = namespaceService.getNamespaceBySlugForRead(
|
||||
slug,
|
||||
|
|
@ -138,4 +155,16 @@ public class NamespacePortalQueryAppService {
|
|||
MemberResponse.from(member, userMap.get(member.getUserId()))
|
||||
));
|
||||
}
|
||||
|
||||
private MyNamespaceResponse toMyNamespaceResponse(Namespace namespace, NamespaceRole role) {
|
||||
return MyNamespaceResponse.from(
|
||||
namespace,
|
||||
role,
|
||||
namespaceAccessPolicy,
|
||||
role != null && namespaceService.canDelete(namespace, role));
|
||||
}
|
||||
|
||||
private boolean isSuperAdmin(Set<String> platformRoles) {
|
||||
return platformRoles != null && platformRoles.contains("SUPER_ADMIN");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,60 @@ class NamespacePortalControllerTest {
|
|||
.andExpect(jsonPath("$.data[0].canDelete").value(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_allowsSuperAdminToSeeAllNamespacesWithoutMembership() throws Exception {
|
||||
Namespace teamA = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
Namespace teamB = namespace(2L, "team-b", NamespaceStatus.ARCHIVED, NamespaceType.TEAM);
|
||||
given(namespaceRepository.findAll()).willReturn(List.of(teamB, teamA));
|
||||
given(namespaceMemberRepository.findByUserId("super-1")).willReturn(List.of());
|
||||
|
||||
mockMvc.perform(get("/api/v1/me/namespaces")
|
||||
.with(auth("super-1", Set.of("SUPER_ADMIN")))
|
||||
.requestAttr("userId", "super-1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data[0].slug").value("team-a"))
|
||||
.andExpect(jsonPath("$.data[0].currentUserRole").doesNotExist())
|
||||
.andExpect(jsonPath("$.data[1].slug").value("team-b"))
|
||||
.andExpect(jsonPath("$.data[1].status").value("ARCHIVED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNamespace_allowsSuperAdminToReadArchivedNamespaceWithoutMembership() throws Exception {
|
||||
Namespace namespace = namespace(1L, "team-a", NamespaceStatus.ARCHIVED, NamespaceType.TEAM);
|
||||
given(namespaceService.getNamespaceBySlug("team-a")).willReturn(namespace);
|
||||
given(namespaceMemberRepository.findByUserId("super-1")).willReturn(List.of());
|
||||
|
||||
mockMvc.perform(get("/api/v1/namespaces/team-a")
|
||||
.with(auth("super-1", Set.of("SUPER_ADMIN")))
|
||||
.requestAttr("userId", "super-1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.slug").value("team-a"))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listNamespaces_allowsSuperAdminToSeeActiveNamespacesWithoutMembership() throws Exception {
|
||||
Namespace teamA = namespace(1L, "team-a", NamespaceStatus.ACTIVE, NamespaceType.TEAM);
|
||||
given(namespaceRepository.findByStatus(eq(NamespaceStatus.ACTIVE), any()))
|
||||
.willReturn(new org.springframework.data.domain.PageImpl<>(
|
||||
List.of(teamA),
|
||||
org.springframework.data.domain.PageRequest.of(0, 20),
|
||||
1
|
||||
));
|
||||
given(namespaceMemberRepository.findByUserId("super-1")).willReturn(List.of());
|
||||
|
||||
mockMvc.perform(get("/api/v1/namespaces")
|
||||
.param("page", "0")
|
||||
.param("size", "20")
|
||||
.with(auth("super-1", Set.of("SUPER_ADMIN")))
|
||||
.requestAttr("userId", "super-1"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.items[0].slug").value("team-a"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNamespace_requiresAuthentication() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/namespaces/team-a"))
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class NamespacePortalQueryAppServiceTest {
|
|||
var response = service.listMyNamespaces(Map.of(
|
||||
2L, NamespaceRole.ADMIN,
|
||||
1L, NamespaceRole.OWNER
|
||||
));
|
||||
), Set.of());
|
||||
|
||||
assertThat(response).hasSize(2);
|
||||
assertThat(response.get(0).slug()).isEqualTo("alpha");
|
||||
|
|
@ -93,7 +93,8 @@ class NamespacePortalQueryAppServiceTest {
|
|||
1L, NamespaceRole.MEMBER,
|
||||
2L, NamespaceRole.ADMIN,
|
||||
3L, NamespaceRole.OWNER
|
||||
)
|
||||
),
|
||||
Set.of()
|
||||
);
|
||||
|
||||
assertThat(response.items()).hasSize(2);
|
||||
|
|
@ -107,10 +108,54 @@ class NamespacePortalQueryAppServiceTest {
|
|||
when(namespaceService.getNamespaceBySlugForRead("team-a", "user-1", Map.of()))
|
||||
.thenReturn(namespace);
|
||||
|
||||
assertThatThrownBy(() -> service.getNamespace("team-a", "user-1", Map.of()))
|
||||
assertThatThrownBy(() -> service.getNamespace("team-a", "user-1", Map.of(), Set.of()))
|
||||
.isInstanceOf(DomainForbiddenException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void listNamespaces_superAdminSeesAllActiveNamespacesWithoutMembership() {
|
||||
Namespace teamA = namespace(1L, "team-a");
|
||||
Namespace teamB = namespace(2L, "team-b");
|
||||
when(namespaceRepository.findByStatus(eq(NamespaceStatus.ACTIVE), any(PageRequest.class)))
|
||||
.thenReturn(new PageImpl<>(List.of(teamA, teamB), PageRequest.of(0, 20), 2));
|
||||
|
||||
var response = service.listNamespaces(PageRequest.of(0, 20), Map.of(), Set.of("SUPER_ADMIN"));
|
||||
|
||||
assertThat(response.items()).extracting("slug").containsExactly("team-a", "team-b");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listMyNamespaces_superAdminSeesAllNamespacesWithoutSyntheticOwnerActions() {
|
||||
Namespace active = namespace(1L, "active-team");
|
||||
Namespace archived = namespace(2L, "archived-team");
|
||||
archived.setStatus(NamespaceStatus.ARCHIVED);
|
||||
when(namespaceRepository.findAll()).thenReturn(List.of(archived, active));
|
||||
when(namespaceAccessPolicy.isImmutable(active)).thenReturn(false);
|
||||
when(namespaceAccessPolicy.isImmutable(archived)).thenReturn(false);
|
||||
|
||||
var response = service.listMyNamespaces(Map.of(), Set.of("SUPER_ADMIN"));
|
||||
|
||||
assertThat(response).hasSize(2);
|
||||
assertThat(response.get(0).slug()).isEqualTo("active-team");
|
||||
assertThat(response.get(0).currentUserRole()).isNull();
|
||||
assertThat(response.get(0).canFreeze()).isFalse();
|
||||
assertThat(response.get(0).canArchive()).isFalse();
|
||||
assertThat(response.get(0).canDelete()).isFalse();
|
||||
assertThat(response.get(1).slug()).isEqualTo("archived-team");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNamespace_superAdminReadsNamespaceWithoutMembership() {
|
||||
Namespace namespace = namespace(1L, "team-a");
|
||||
namespace.setStatus(NamespaceStatus.ARCHIVED);
|
||||
when(namespaceService.getNamespaceBySlug("team-a")).thenReturn(namespace);
|
||||
|
||||
var response = service.getNamespace("team-a", "super-1", Map.of(), Set.of("SUPER_ADMIN"));
|
||||
|
||||
assertThat(response.slug()).isEqualTo("team-a");
|
||||
assertThat(response.status()).isEqualTo(NamespaceStatus.ARCHIVED);
|
||||
}
|
||||
|
||||
private Namespace namespace(Long id, String slug) {
|
||||
Namespace namespace = new Namespace(slug, slug, "owner-1");
|
||||
ReflectionTestUtils.setField(namespace, "id", id);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import java.util.Optional;
|
|||
*/
|
||||
public interface NamespaceRepository {
|
||||
Optional<Namespace> findById(Long id);
|
||||
List<Namespace> findAll();
|
||||
List<Namespace> findByIdIn(List<Long> ids);
|
||||
Optional<Namespace> findBySlug(String slug);
|
||||
Page<Namespace> findByStatus(NamespaceStatus status, Pageable pageable);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue