From 23d4c21054bc5d4de9d98e27375b10d01ce65faf Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Sat, 14 Feb 2026 11:40:18 +0800 Subject: [PATCH 01/12] feat: add OpenAPI User Management with create, retrieve, and search functionalities --- .../apollo/openapi/dto/OpenUserDTO.java | 82 ++++ .../openapi/v1/controller/UserController.java | 154 +++++++ .../v1/controller/UserControllerTest.java | 419 ++++++++++++++++++ 3 files changed, 655 insertions(+) create mode 100644 apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java create mode 100644 apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java create mode 100644 apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java new file mode 100644 index 00000000000..6602f368c26 --- /dev/null +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.openapi.dto; + +/** + * Open API User DTO for user management operations. + * + * @author dreamweaver + */ +public class OpenUserDTO { + + private String username; + private String userDisplayName; + private String password; + private String email; + private Integer enabled; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getUserDisplayName() { + return userDisplayName; + } + + public void setUserDisplayName(String userDisplayName) { + this.userDisplayName = userDisplayName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Integer getEnabled() { + return enabled; + } + + public void setEnabled(Integer enabled) { + this.enabled = enabled; + } + + @Override + public String toString() { + return "OpenUserDTO{" + + "username='" + username + '\'' + + ", userDisplayName='" + userDisplayName + '\'' + + ", email='" + email + '\'' + + ", enabled=" + enabled + + '}'; + } +} + diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java new file mode 100644 index 00000000000..f0827fe4dab --- /dev/null +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -0,0 +1,154 @@ +/* + * Copyright 2025 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.openapi.v1.controller; + +import com.ctrip.framework.apollo.audit.annotation.ApolloAuditLog; +import com.ctrip.framework.apollo.audit.annotation.OpType; +import com.ctrip.framework.apollo.common.exception.BadRequestException; +import com.ctrip.framework.apollo.core.utils.StringUtils; +import com.ctrip.framework.apollo.openapi.dto.OpenUserDTO; +import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; +import com.ctrip.framework.apollo.portal.entity.po.UserPO; +import com.ctrip.framework.apollo.portal.spi.UserService; +import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; +import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; +import com.ctrip.framework.apollo.portal.util.checker.CheckResult; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * OpenAPI User Management Controller Provides RESTful APIs for user management operations through + * OpenAPI + * + * @author dreamweaver + */ +@RestController("openapiUserController") +@RequestMapping("/openapi/v1") +public class UserController { + + private static final int DEFAULT_USER_ENABLED = 1; + + private final UserService userService; + private final AuthUserPasswordChecker passwordChecker; + + public UserController(final UserService userService, + final AuthUserPasswordChecker passwordChecker) { + this.userService = userService; + this.passwordChecker = passwordChecker; + } + + /** + * Create a new user + * + * @param openUserDTO user information to create + * @return ResponseEntity with no content on success + */ + @ApolloAuditLog(name = "OpenAPI.createUser", type = OpType.CREATE, description = "Create user via OpenAPI") + @PostMapping("/users") + public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) { + // Validate required fields + if (StringUtils.isContainEmpty(openUserDTO.getUsername(), openUserDTO.getPassword())) { + throw new BadRequestException("Username and password cannot be empty."); + } + + if (StringUtils.isEmpty(openUserDTO.getEmail())) { + throw new BadRequestException("Email cannot be empty."); + } + + // Check password strength + CheckResult pwdCheckRes = passwordChecker.checkWeakPassword(openUserDTO.getPassword()); + if (!pwdCheckRes.isSuccess()) { + throw new BadRequestException(pwdCheckRes.getMessage()); + } + + // Check if UserService supports user creation + if (!(userService instanceof SpringSecurityUserService)) { + throw new UnsupportedOperationException( + "Create user operation is not supported with current user service implementation"); + } + + // Convert DTO to PO and set defaults + UserPO userPO = new UserPO(); + userPO.setUsername(openUserDTO.getUsername()); + userPO.setPassword(openUserDTO.getPassword()); + userPO.setEmail(openUserDTO.getEmail()); + userPO.setUserDisplayName(openUserDTO.getUserDisplayName() != null + ? openUserDTO.getUserDisplayName() + : openUserDTO.getUsername()); + userPO.setEnabled(openUserDTO.getEnabled() != null + ? openUserDTO.getEnabled() + : DEFAULT_USER_ENABLED); + + // Create user + ((SpringSecurityUserService) userService).create(userPO); + + return ResponseEntity.ok().build(); + } + + /** + * Get user information by user ID + * + * @param userId the user ID to query + * @return UserInfo object + */ + @GetMapping("/users/{userId}") + public ResponseEntity getUserByUserId(@PathVariable String userId) { + UserInfo userInfo = userService.findByUserId(userId); + if (userInfo == null) { + throw new BadRequestException("User not found: " + userId); + } + return ResponseEntity.ok(userInfo); + } + + /** + * Search users by keyword + * + * @param keyword keyword to search (searches in username and display name) + * @param includeInactiveUsers whether to include inactive users + * @param offset pagination offset + * @param limit pagination limit + * @return list of UserInfo objects + */ + @GetMapping("/users") + public ResponseEntity> searchUsers( + @RequestParam(value = "keyword", required = false, defaultValue = "") String keyword, + @RequestParam(value = "includeInactiveUsers", defaultValue = "false") boolean includeInactiveUsers, + @RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "limit", defaultValue = "10") int limit) { + + if (limit <= 0 || limit > 100) { + throw new BadRequestException("Limit must be between 1 and 100"); + } + + if (offset < 0) { + throw new BadRequestException("Offset must be non-negative"); + } + + List users = userService.searchUsers(keyword, offset, limit, includeInactiveUsers); + return ResponseEntity.ok(users); + } +} + diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java new file mode 100644 index 00000000000..4f6913d2b20 --- /dev/null +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java @@ -0,0 +1,419 @@ +/* + * Copyright 2025 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.openapi.v1.controller; + +import com.ctrip.framework.apollo.common.exception.BadRequestException; +import com.ctrip.framework.apollo.openapi.dto.OpenUserDTO; +import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; +import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; +import com.ctrip.framework.apollo.portal.entity.po.UserPO; +import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; +import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; +import com.ctrip.framework.apollo.portal.util.checker.CheckResult; +import com.google.gson.Gson; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Unit tests for OpenAPI UserController + * + * @author dreamweaver + */ +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(properties = { + "spring.profiles.active=github,auth" +}) +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SpringSecurityUserService userService; + + @MockBean + private AuthUserPasswordChecker passwordChecker; + + @MockBean(name = "unifiedPermissionValidator") + private UnifiedPermissionValidator unifiedPermissionValidator; + + private final Gson gson = new Gson(); + + @Before + public void setUp() { + // Mock super admin permission by default + when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(true); + } + + @Test + public void testCreateUser_Success() throws Exception { + // Arrange + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername("testuser"); + openUserDTO.setPassword("StrongP@ssw0rd"); + openUserDTO.setEmail("testuser@example.com"); + openUserDTO.setUserDisplayName("Test User"); + openUserDTO.setEnabled(1); + + CheckResult checkResult = new CheckResult(true, ""); + when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); + doNothing().when(userService).create(any(UserPO.class)); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isOk()); + + // Verify + ArgumentCaptor userPOCaptor = ArgumentCaptor.forClass(UserPO.class); + verify(userService, times(1)).create(userPOCaptor.capture()); + + UserPO capturedUser = userPOCaptor.getValue(); + assertEquals("testuser", capturedUser.getUsername()); + assertEquals("testuser@example.com", capturedUser.getEmail()); + assertEquals("Test User", capturedUser.getUserDisplayName()); + assertEquals(1, capturedUser.getEnabled()); + assertNotNull(capturedUser.getPassword()); + } + + @Test + public void testCreateUser_WithDefaultValues() throws Exception { + // Arrange + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername("testuser2"); + openUserDTO.setPassword("StrongP@ssw0rd"); + openUserDTO.setEmail("testuser2@example.com"); + // Not setting userDisplayName and enabled + + CheckResult checkResult = new CheckResult(true, ""); + when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); + doNothing().when(userService).create(any(UserPO.class)); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isOk()); + + // Verify default values are set + ArgumentCaptor userPOCaptor = ArgumentCaptor.forClass(UserPO.class); + verify(userService, times(1)).create(userPOCaptor.capture()); + + UserPO capturedUser = userPOCaptor.getValue(); + assertEquals("testuser2", capturedUser.getUserDisplayName()); // Should default to username + assertEquals(1, capturedUser.getEnabled()); // Should default to 1 + } + + @Test + public void testCreateUser_EmptyUsername() throws Exception { + // Arrange + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername(""); + openUserDTO.setPassword("StrongP@ssw0rd"); + openUserDTO.setEmail("test@example.com"); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isBadRequest()); + + // Verify that create was never called + verify(userService, times(0)).create(any(UserPO.class)); + } + + @Test + public void testCreateUser_EmptyPassword() throws Exception { + // Arrange + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername("testuser"); + openUserDTO.setPassword(""); + openUserDTO.setEmail("test@example.com"); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isBadRequest()); + + verify(userService, times(0)).create(any(UserPO.class)); + } + + @Test + public void testCreateUser_EmptyEmail() throws Exception { + // Arrange + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername("testuser"); + openUserDTO.setPassword("StrongP@ssw0rd"); + openUserDTO.setEmail(""); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isBadRequest()); + + verify(userService, times(0)).create(any(UserPO.class)); + } + + @Test + public void testCreateUser_WeakPassword() throws Exception { + // Arrange + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername("testuser"); + openUserDTO.setPassword("weak"); + openUserDTO.setEmail("test@example.com"); + + CheckResult checkResult = new CheckResult(false, "Password is too weak"); + when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isBadRequest()); + + verify(userService, times(0)).create(any(UserPO.class)); + } + + @Test + public void testCreateUser_UserAlreadyExists() throws Exception { + // Arrange + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername("existinguser"); + openUserDTO.setPassword("StrongP@ssw0rd"); + openUserDTO.setEmail("test@example.com"); + + CheckResult checkResult = new CheckResult(true, ""); + when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); + doThrow(BadRequestException.userAlreadyExists("existinguser")) + .when(userService).create(any(UserPO.class)); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isBadRequest()); + } + + @Test + public void testCreateUser_NoSuperAdminPermission() throws Exception { + // Arrange + when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); + + OpenUserDTO openUserDTO = new OpenUserDTO(); + openUserDTO.setUsername("testuser"); + openUserDTO.setPassword("StrongP@ssw0rd"); + openUserDTO.setEmail("test@example.com"); + + // Act & Assert + mockMvc.perform(post("/openapi/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))) + .andExpect(status().isForbidden()); + + verify(userService, times(0)).create(any(UserPO.class)); + } + + @Test + public void testGetUserByUserId_Success() throws Exception { + // Arrange + String userId = "testuser"; + UserInfo userInfo = new UserInfo(); + userInfo.setUserId(userId); + userInfo.setName("Test User"); + userInfo.setEmail("testuser@example.com"); + + when(userService.findByUserId(userId)).thenReturn(userInfo); + + // Act & Assert + mockMvc.perform(get("/openapi/v1/users/" + userId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value(userId)) + .andExpect(jsonPath("$.name").value("Test User")) + .andExpect(jsonPath("$.email").value("testuser@example.com")); + + verify(userService, times(1)).findByUserId(userId); + } + + @Test + public void testGetUserByUserId_UserNotFound() throws Exception { + // Arrange + String userId = "nonexistent"; + when(userService.findByUserId(userId)).thenReturn(null); + + // Act & Assert + mockMvc.perform(get("/openapi/v1/users/" + userId)) + .andExpect(status().isBadRequest()); + + verify(userService, times(1)).findByUserId(userId); + } + + @Test + public void testGetUserByUserId_NoSuperAdminPermission() throws Exception { + // Arrange + when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); + + // Act & Assert + mockMvc.perform(get("/openapi/v1/users/testuser")) + .andExpect(status().isForbidden()); + + verify(userService, times(0)).findByUserId(anyString()); + } + + @Test + public void testSearchUsers_Success() throws Exception { + // Arrange + UserInfo user1 = new UserInfo(); + user1.setUserId("user1"); + user1.setName("User One"); + + UserInfo user2 = new UserInfo(); + user2.setUserId("user2"); + user2.setName("User Two"); + + List users = Arrays.asList(user1, user2); + when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())) + .thenReturn(users); + + // Act & Assert + mockMvc.perform(get("/openapi/v1/users") + .param("keyword", "user") + .param("offset", "0") + .param("limit", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(2)) + .andExpect(jsonPath("$[0].userId").value("user1")) + .andExpect(jsonPath("$[1].userId").value("user2")); + + verify(userService, times(1)).searchUsers("user", 0, 10, false); + } + + @Test + public void testSearchUsers_WithIncludeInactiveUsers() throws Exception { + // Arrange + when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())) + .thenReturn(Collections.emptyList()); + + // Act & Assert + mockMvc.perform(get("/openapi/v1/users") + .param("keyword", "test") + .param("includeInactiveUsers", "true") + .param("offset", "0") + .param("limit", "20")) + .andExpect(status().isOk()); + + verify(userService, times(1)).searchUsers("test", 0, 20, true); + } + + @Test + public void testSearchUsers_DefaultParameters() throws Exception { + // Arrange + when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())) + .thenReturn(Collections.emptyList()); + + // Act & Assert + mockMvc.perform(get("/openapi/v1/users")) + .andExpect(status().isOk()); + + // Verify default values are used + verify(userService, times(1)).searchUsers("", 0, 10, false); + } + + @Test + public void testSearchUsers_InvalidLimit() throws Exception { + // Act & Assert - limit too high + mockMvc.perform(get("/openapi/v1/users") + .param("limit", "101")) + .andExpect(status().isBadRequest()); + + // Act & Assert - limit zero + mockMvc.perform(get("/openapi/v1/users") + .param("limit", "0")) + .andExpect(status().isBadRequest()); + + // Act & Assert - limit negative + mockMvc.perform(get("/openapi/v1/users") + .param("limit", "-1")) + .andExpect(status().isBadRequest()); + + verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); + } + + @Test + public void testSearchUsers_InvalidOffset() throws Exception { + // Act & Assert + mockMvc.perform(get("/openapi/v1/users") + .param("offset", "-1")) + .andExpect(status().isBadRequest()); + + verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); + } + + @Test + public void testSearchUsers_NoSuperAdminPermission() throws Exception { + // Arrange + when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); + + // Act & Assert + mockMvc.perform(get("/openapi/v1/users")) + .andExpect(status().isForbidden()); + + verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); + } +} + + + + + + + + From 3c2d02bd8c9706fe20209583c937aafdc24d2d6e Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Mon, 16 Feb 2026 09:56:14 +0800 Subject: [PATCH 02/12] feat: update createUser endpoint to return created user information --- .../apollo/openapi/v1/controller/UserController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index f0827fe4dab..107e76606a5 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -64,11 +64,11 @@ public UserController(final UserService userService, * Create a new user * * @param openUserDTO user information to create - * @return ResponseEntity with no content on success + * @return ResponseEntity with created user information */ @ApolloAuditLog(name = "OpenAPI.createUser", type = OpType.CREATE, description = "Create user via OpenAPI") @PostMapping("/users") - public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) { + public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) { // Validate required fields if (StringUtils.isContainEmpty(openUserDTO.getUsername(), openUserDTO.getPassword())) { throw new BadRequestException("Username and password cannot be empty."); @@ -105,7 +105,9 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) { // Create user ((SpringSecurityUserService) userService).create(userPO); - return ResponseEntity.ok().build(); + // Retrieve and return the created user information + UserInfo createdUser = userService.findByUserId(openUserDTO.getUsername()); + return ResponseEntity.ok(createdUser); } /** From 4c70231469519e41366f79197ac459ce03ff1a9d Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Mon, 16 Feb 2026 10:02:30 +0800 Subject: [PATCH 03/12] refactor: simplify formatting in OpenUserDTO and UserController --- .../apollo/openapi/dto/OpenUserDTO.java | 9 +- .../openapi/v1/controller/UserController.java | 19 ++-- .../v1/controller/UserControllerTest.java | 87 ++++++------------- 3 files changed, 38 insertions(+), 77 deletions(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java index 6602f368c26..32b3e4aca8a 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/dto/OpenUserDTO.java @@ -71,12 +71,7 @@ public void setEnabled(Integer enabled) { @Override public String toString() { - return "OpenUserDTO{" + - "username='" + username + '\'' + - ", userDisplayName='" + userDisplayName + '\'' + - ", email='" + email + '\'' + - ", enabled=" + enabled + - '}'; + return "OpenUserDTO{" + "username='" + username + '\'' + ", userDisplayName='" + userDisplayName + + '\'' + ", email='" + email + '\'' + ", enabled=" + enabled + '}'; } } - diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index 107e76606a5..dfe7c4b3b89 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -28,7 +28,6 @@ import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; import com.ctrip.framework.apollo.portal.util.checker.CheckResult; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -66,7 +65,8 @@ public UserController(final UserService userService, * @param openUserDTO user information to create * @return ResponseEntity with created user information */ - @ApolloAuditLog(name = "OpenAPI.createUser", type = OpType.CREATE, description = "Create user via OpenAPI") + @ApolloAuditLog(name = "OpenAPI.createUser", type = OpType.CREATE, + description = "Create user via OpenAPI") @PostMapping("/users") public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) { // Validate required fields @@ -95,12 +95,11 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) userPO.setUsername(openUserDTO.getUsername()); userPO.setPassword(openUserDTO.getPassword()); userPO.setEmail(openUserDTO.getEmail()); - userPO.setUserDisplayName(openUserDTO.getUserDisplayName() != null - ? openUserDTO.getUserDisplayName() - : openUserDTO.getUsername()); - userPO.setEnabled(openUserDTO.getEnabled() != null - ? openUserDTO.getEnabled() - : DEFAULT_USER_ENABLED); + userPO.setUserDisplayName( + openUserDTO.getUserDisplayName() != null ? openUserDTO.getUserDisplayName() + : openUserDTO.getUsername()); + userPO.setEnabled( + openUserDTO.getEnabled() != null ? openUserDTO.getEnabled() : DEFAULT_USER_ENABLED); // Create user ((SpringSecurityUserService) userService).create(userPO); @@ -137,7 +136,8 @@ public ResponseEntity getUserByUserId(@PathVariable String userId) { @GetMapping("/users") public ResponseEntity> searchUsers( @RequestParam(value = "keyword", required = false, defaultValue = "") String keyword, - @RequestParam(value = "includeInactiveUsers", defaultValue = "false") boolean includeInactiveUsers, + @RequestParam(value = "includeInactiveUsers", + defaultValue = "false") boolean includeInactiveUsers, @RequestParam(value = "offset", defaultValue = "0") int offset, @RequestParam(value = "limit", defaultValue = "10") int limit) { @@ -153,4 +153,3 @@ public ResponseEntity> searchUsers( return ResponseEntity.ok(users); } } - diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java index 4f6913d2b20..ae3fb9c3536 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java @@ -66,9 +66,7 @@ @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc(addFilters = false) -@TestPropertySource(properties = { - "spring.profiles.active=github,auth" -}) +@TestPropertySource(properties = {"spring.profiles.active=github,auth"}) public class UserControllerTest { @Autowired @@ -106,10 +104,8 @@ public void testCreateUser_Success() throws Exception { doNothing().when(userService).create(any(UserPO.class)); // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isOk()); + mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))).andExpect(status().isOk()); // Verify ArgumentCaptor userPOCaptor = ArgumentCaptor.forClass(UserPO.class); @@ -137,10 +133,8 @@ public void testCreateUser_WithDefaultValues() throws Exception { doNothing().when(userService).create(any(UserPO.class)); // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isOk()); + mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))).andExpect(status().isOk()); // Verify default values are set ArgumentCaptor userPOCaptor = ArgumentCaptor.forClass(UserPO.class); @@ -160,10 +154,8 @@ public void testCreateUser_EmptyUsername() throws Exception { openUserDTO.setEmail("test@example.com"); // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isBadRequest()); + mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); // Verify that create was never called verify(userService, times(0)).create(any(UserPO.class)); @@ -178,10 +170,8 @@ public void testCreateUser_EmptyPassword() throws Exception { openUserDTO.setEmail("test@example.com"); // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isBadRequest()); + mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); verify(userService, times(0)).create(any(UserPO.class)); } @@ -195,10 +185,8 @@ public void testCreateUser_EmptyEmail() throws Exception { openUserDTO.setEmail(""); // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isBadRequest()); + mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); verify(userService, times(0)).create(any(UserPO.class)); } @@ -215,10 +203,8 @@ public void testCreateUser_WeakPassword() throws Exception { when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isBadRequest()); + mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); verify(userService, times(0)).create(any(UserPO.class)); } @@ -233,14 +219,12 @@ public void testCreateUser_UserAlreadyExists() throws Exception { CheckResult checkResult = new CheckResult(true, ""); when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); - doThrow(BadRequestException.userAlreadyExists("existinguser")) - .when(userService).create(any(UserPO.class)); + doThrow(BadRequestException.userAlreadyExists("existinguser")).when(userService) + .create(any(UserPO.class)); // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isBadRequest()); + mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) + .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); } @Test @@ -274,8 +258,7 @@ public void testGetUserByUserId_Success() throws Exception { when(userService.findByUserId(userId)).thenReturn(userInfo); // Act & Assert - mockMvc.perform(get("/openapi/v1/users/" + userId)) - .andExpect(status().isOk()) + mockMvc.perform(get("/openapi/v1/users/" + userId)).andExpect(status().isOk()) .andExpect(jsonPath("$.userId").value(userId)) .andExpect(jsonPath("$.name").value("Test User")) .andExpect(jsonPath("$.email").value("testuser@example.com")); @@ -290,8 +273,7 @@ public void testGetUserByUserId_UserNotFound() throws Exception { when(userService.findByUserId(userId)).thenReturn(null); // Act & Assert - mockMvc.perform(get("/openapi/v1/users/" + userId)) - .andExpect(status().isBadRequest()); + mockMvc.perform(get("/openapi/v1/users/" + userId)).andExpect(status().isBadRequest()); verify(userService, times(1)).findByUserId(userId); } @@ -320,16 +302,13 @@ public void testSearchUsers_Success() throws Exception { user2.setName("User Two"); List users = Arrays.asList(user1, user2); - when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())) - .thenReturn(users); + when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())).thenReturn(users); // Act & Assert - mockMvc.perform(get("/openapi/v1/users") - .param("keyword", "user") - .param("offset", "0") + mockMvc + .perform(get("/openapi/v1/users").param("keyword", "user").param("offset", "0") .param("limit", "10")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(2)) + .andExpect(status().isOk()).andExpect(jsonPath("$.length()").value(2)) .andExpect(jsonPath("$[0].userId").value("user1")) .andExpect(jsonPath("$[1].userId").value("user2")); @@ -370,18 +349,15 @@ public void testSearchUsers_DefaultParameters() throws Exception { @Test public void testSearchUsers_InvalidLimit() throws Exception { // Act & Assert - limit too high - mockMvc.perform(get("/openapi/v1/users") - .param("limit", "101")) + mockMvc.perform(get("/openapi/v1/users").param("limit", "101")) .andExpect(status().isBadRequest()); // Act & Assert - limit zero - mockMvc.perform(get("/openapi/v1/users") - .param("limit", "0")) + mockMvc.perform(get("/openapi/v1/users").param("limit", "0")) .andExpect(status().isBadRequest()); // Act & Assert - limit negative - mockMvc.perform(get("/openapi/v1/users") - .param("limit", "-1")) + mockMvc.perform(get("/openapi/v1/users").param("limit", "-1")) .andExpect(status().isBadRequest()); verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); @@ -390,8 +366,7 @@ public void testSearchUsers_InvalidLimit() throws Exception { @Test public void testSearchUsers_InvalidOffset() throws Exception { // Act & Assert - mockMvc.perform(get("/openapi/v1/users") - .param("offset", "-1")) + mockMvc.perform(get("/openapi/v1/users").param("offset", "-1")) .andExpect(status().isBadRequest()); verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); @@ -409,11 +384,3 @@ public void testSearchUsers_NoSuperAdminPermission() throws Exception { verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); } } - - - - - - - - From a70c64ec7d076d6f3aad95e8557476f1f93f0263 Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Tue, 17 Feb 2026 15:21:48 +0800 Subject: [PATCH 04/12] feat: add super admin authorization to user management endpoints --- .../openapi/v1/controller/UserController.java | 4 + .../v1/controller/UserControllerTest.java | 217 ++++++++---------- 2 files changed, 96 insertions(+), 125 deletions(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index dfe7c4b3b89..d420da0db75 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -28,6 +28,7 @@ import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; import com.ctrip.framework.apollo.portal.util.checker.CheckResult; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -65,6 +66,7 @@ public UserController(final UserService userService, * @param openUserDTO user information to create * @return ResponseEntity with created user information */ + @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @ApolloAuditLog(name = "OpenAPI.createUser", type = OpType.CREATE, description = "Create user via OpenAPI") @PostMapping("/users") @@ -115,6 +117,7 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) * @param userId the user ID to query * @return UserInfo object */ + @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/users/{userId}") public ResponseEntity getUserByUserId(@PathVariable String userId) { UserInfo userInfo = userService.findByUserId(userId); @@ -133,6 +136,7 @@ public ResponseEntity getUserByUserId(@PathVariable String userId) { * @param limit pagination limit * @return list of UserInfo objects */ + @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/users") public ResponseEntity> searchUsers( @RequestParam(value = "keyword", required = false, defaultValue = "") String keyword, diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java index ae3fb9c3536..153393b92f3 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java @@ -18,7 +18,6 @@ import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.openapi.dto.OpenUserDTO; -import com.ctrip.framework.apollo.portal.component.UnifiedPermissionValidator; import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; @@ -29,14 +28,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.ResponseEntity; import java.util.Arrays; import java.util.Collections; @@ -44,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -53,44 +48,32 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * Unit tests for OpenAPI UserController * * @author dreamweaver */ -@RunWith(SpringRunner.class) -@SpringBootTest -@AutoConfigureMockMvc(addFilters = false) -@TestPropertySource(properties = {"spring.profiles.active=github,auth"}) +@RunWith(MockitoJUnitRunner.class) public class UserControllerTest { - @Autowired - private MockMvc mockMvc; - - @MockBean + @Mock private SpringSecurityUserService userService; - @MockBean + @Mock private AuthUserPasswordChecker passwordChecker; - @MockBean(name = "unifiedPermissionValidator") - private UnifiedPermissionValidator unifiedPermissionValidator; + private UserController userController; private final Gson gson = new Gson(); @Before public void setUp() { - // Mock super admin permission by default - when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(true); + userController = new UserController(userService, passwordChecker); } @Test - public void testCreateUser_Success() throws Exception { + public void testCreateUser_Success() { // Arrange OpenUserDTO openUserDTO = new OpenUserDTO(); openUserDTO.setUsername("testuser"); @@ -103,9 +86,19 @@ public void testCreateUser_Success() throws Exception { when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); doNothing().when(userService).create(any(UserPO.class)); - // Act & Assert - mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))).andExpect(status().isOk()); + UserInfo createdUser = new UserInfo(); + createdUser.setUserId("testuser"); + createdUser.setName("Test User"); + createdUser.setEmail("testuser@example.com"); + when(userService.findByUserId("testuser")).thenReturn(createdUser); + + // Act + ResponseEntity response = userController.createUser(openUserDTO); + + // Assert + assertEquals(200, response.getStatusCodeValue()); + assertNotNull(response.getBody()); + assertEquals("testuser", response.getBody().getUserId()); // Verify ArgumentCaptor userPOCaptor = ArgumentCaptor.forClass(UserPO.class); @@ -120,7 +113,7 @@ public void testCreateUser_Success() throws Exception { } @Test - public void testCreateUser_WithDefaultValues() throws Exception { + public void testCreateUser_WithDefaultValues() { // Arrange OpenUserDTO openUserDTO = new OpenUserDTO(); openUserDTO.setUsername("testuser2"); @@ -132,9 +125,15 @@ public void testCreateUser_WithDefaultValues() throws Exception { when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); doNothing().when(userService).create(any(UserPO.class)); - // Act & Assert - mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))).andExpect(status().isOk()); + UserInfo createdUser = new UserInfo(); + createdUser.setUserId("testuser2"); + when(userService.findByUserId("testuser2")).thenReturn(createdUser); + + // Act + ResponseEntity response = userController.createUser(openUserDTO); + + // Assert + assertEquals(200, response.getStatusCodeValue()); // Verify default values are set ArgumentCaptor userPOCaptor = ArgumentCaptor.forClass(UserPO.class); @@ -146,7 +145,7 @@ public void testCreateUser_WithDefaultValues() throws Exception { } @Test - public void testCreateUser_EmptyUsername() throws Exception { + public void testCreateUser_EmptyUsername() { // Arrange OpenUserDTO openUserDTO = new OpenUserDTO(); openUserDTO.setUsername(""); @@ -154,15 +153,14 @@ public void testCreateUser_EmptyUsername() throws Exception { openUserDTO.setEmail("test@example.com"); // Act & Assert - mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); + assertThrows(BadRequestException.class, () -> userController.createUser(openUserDTO)); // Verify that create was never called verify(userService, times(0)).create(any(UserPO.class)); } @Test - public void testCreateUser_EmptyPassword() throws Exception { + public void testCreateUser_EmptyPassword() { // Arrange OpenUserDTO openUserDTO = new OpenUserDTO(); openUserDTO.setUsername("testuser"); @@ -170,14 +168,13 @@ public void testCreateUser_EmptyPassword() throws Exception { openUserDTO.setEmail("test@example.com"); // Act & Assert - mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); + assertThrows(BadRequestException.class, () -> userController.createUser(openUserDTO)); verify(userService, times(0)).create(any(UserPO.class)); } @Test - public void testCreateUser_EmptyEmail() throws Exception { + public void testCreateUser_EmptyEmail() { // Arrange OpenUserDTO openUserDTO = new OpenUserDTO(); openUserDTO.setUsername("testuser"); @@ -185,14 +182,13 @@ public void testCreateUser_EmptyEmail() throws Exception { openUserDTO.setEmail(""); // Act & Assert - mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); + assertThrows(BadRequestException.class, () -> userController.createUser(openUserDTO)); verify(userService, times(0)).create(any(UserPO.class)); } @Test - public void testCreateUser_WeakPassword() throws Exception { + public void testCreateUser_WeakPassword() { // Arrange OpenUserDTO openUserDTO = new OpenUserDTO(); openUserDTO.setUsername("testuser"); @@ -203,14 +199,13 @@ public void testCreateUser_WeakPassword() throws Exception { when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); // Act & Assert - mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); + assertThrows(BadRequestException.class, () -> userController.createUser(openUserDTO)); verify(userService, times(0)).create(any(UserPO.class)); } @Test - public void testCreateUser_UserAlreadyExists() throws Exception { + public void testCreateUser_UserAlreadyExists() { // Arrange OpenUserDTO openUserDTO = new OpenUserDTO(); openUserDTO.setUsername("existinguser"); @@ -223,31 +218,12 @@ public void testCreateUser_UserAlreadyExists() throws Exception { .create(any(UserPO.class)); // Act & Assert - mockMvc.perform(post("/openapi/v1/users").contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))).andExpect(status().isBadRequest()); + assertThrows(BadRequestException.class, () -> userController.createUser(openUserDTO)); } - @Test - public void testCreateUser_NoSuperAdminPermission() throws Exception { - // Arrange - when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); - - OpenUserDTO openUserDTO = new OpenUserDTO(); - openUserDTO.setUsername("testuser"); - openUserDTO.setPassword("StrongP@ssw0rd"); - openUserDTO.setEmail("test@example.com"); - - // Act & Assert - mockMvc.perform(post("/openapi/v1/users") - .contentType(MediaType.APPLICATION_JSON) - .content(gson.toJson(openUserDTO))) - .andExpect(status().isForbidden()); - - verify(userService, times(0)).create(any(UserPO.class)); - } @Test - public void testGetUserByUserId_Success() throws Exception { + public void testGetUserByUserId_Success() { // Arrange String userId = "testuser"; UserInfo userInfo = new UserInfo(); @@ -257,41 +233,34 @@ public void testGetUserByUserId_Success() throws Exception { when(userService.findByUserId(userId)).thenReturn(userInfo); - // Act & Assert - mockMvc.perform(get("/openapi/v1/users/" + userId)).andExpect(status().isOk()) - .andExpect(jsonPath("$.userId").value(userId)) - .andExpect(jsonPath("$.name").value("Test User")) - .andExpect(jsonPath("$.email").value("testuser@example.com")); + // Act + ResponseEntity response = userController.getUserByUserId(userId); + + // Assert + assertEquals(200, response.getStatusCodeValue()); + assertNotNull(response.getBody()); + assertEquals(userId, response.getBody().getUserId()); + assertEquals("Test User", response.getBody().getName()); + assertEquals("testuser@example.com", response.getBody().getEmail()); verify(userService, times(1)).findByUserId(userId); } @Test - public void testGetUserByUserId_UserNotFound() throws Exception { + public void testGetUserByUserId_UserNotFound() { // Arrange String userId = "nonexistent"; when(userService.findByUserId(userId)).thenReturn(null); // Act & Assert - mockMvc.perform(get("/openapi/v1/users/" + userId)).andExpect(status().isBadRequest()); + assertThrows(BadRequestException.class, () -> userController.getUserByUserId(userId)); verify(userService, times(1)).findByUserId(userId); } - @Test - public void testGetUserByUserId_NoSuperAdminPermission() throws Exception { - // Arrange - when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); - - // Act & Assert - mockMvc.perform(get("/openapi/v1/users/testuser")) - .andExpect(status().isForbidden()); - - verify(userService, times(0)).findByUserId(anyString()); - } @Test - public void testSearchUsers_Success() throws Exception { + public void testSearchUsers_Success() { // Arrange UserInfo user1 = new UserInfo(); user1.setUserId("user1"); @@ -304,82 +273,80 @@ public void testSearchUsers_Success() throws Exception { List users = Arrays.asList(user1, user2); when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())).thenReturn(users); - // Act & Assert - mockMvc - .perform(get("/openapi/v1/users").param("keyword", "user").param("offset", "0") - .param("limit", "10")) - .andExpect(status().isOk()).andExpect(jsonPath("$.length()").value(2)) - .andExpect(jsonPath("$[0].userId").value("user1")) - .andExpect(jsonPath("$[1].userId").value("user2")); + // Act + ResponseEntity> response = userController.searchUsers("user", false, 0, 10); + + // Assert + assertEquals(200, response.getStatusCodeValue()); + assertNotNull(response.getBody()); + assertEquals(2, response.getBody().size()); + assertEquals("user1", response.getBody().get(0).getUserId()); + assertEquals("user2", response.getBody().get(1).getUserId()); verify(userService, times(1)).searchUsers("user", 0, 10, false); } @Test - public void testSearchUsers_WithIncludeInactiveUsers() throws Exception { + public void testSearchUsers_WithIncludeInactiveUsers() { // Arrange when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())) .thenReturn(Collections.emptyList()); - // Act & Assert - mockMvc.perform(get("/openapi/v1/users") - .param("keyword", "test") - .param("includeInactiveUsers", "true") - .param("offset", "0") - .param("limit", "20")) - .andExpect(status().isOk()); + // Act + ResponseEntity> response = userController.searchUsers("test", true, 0, 20); + + // Assert + assertEquals(200, response.getStatusCodeValue()); verify(userService, times(1)).searchUsers("test", 0, 20, true); } @Test - public void testSearchUsers_DefaultParameters() throws Exception { + public void testSearchUsers_DefaultParameters() { // Arrange when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())) .thenReturn(Collections.emptyList()); - // Act & Assert - mockMvc.perform(get("/openapi/v1/users")) - .andExpect(status().isOk()); + // Act + ResponseEntity> response = userController.searchUsers("", false, 0, 10); + + // Assert + assertEquals(200, response.getStatusCodeValue()); // Verify default values are used verify(userService, times(1)).searchUsers("", 0, 10, false); } @Test - public void testSearchUsers_InvalidLimit() throws Exception { - // Act & Assert - limit too high - mockMvc.perform(get("/openapi/v1/users").param("limit", "101")) - .andExpect(status().isBadRequest()); + public void testSearchUsers_InvalidLimit_TooHigh() { + // Act & Assert + assertThrows(BadRequestException.class, + () -> userController.searchUsers("test", false, 0, 101)); - // Act & Assert - limit zero - mockMvc.perform(get("/openapi/v1/users").param("limit", "0")) - .andExpect(status().isBadRequest()); + verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); + } - // Act & Assert - limit negative - mockMvc.perform(get("/openapi/v1/users").param("limit", "-1")) - .andExpect(status().isBadRequest()); + @Test + public void testSearchUsers_InvalidLimit_Zero() { + // Act & Assert + assertThrows(BadRequestException.class, () -> userController.searchUsers("test", false, 0, 0)); verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); } @Test - public void testSearchUsers_InvalidOffset() throws Exception { + public void testSearchUsers_InvalidLimit_Negative() { // Act & Assert - mockMvc.perform(get("/openapi/v1/users").param("offset", "-1")) - .andExpect(status().isBadRequest()); + assertThrows(BadRequestException.class, () -> userController.searchUsers("test", false, 0, -1)); verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); } @Test - public void testSearchUsers_NoSuperAdminPermission() throws Exception { - // Arrange - when(unifiedPermissionValidator.isSuperAdmin()).thenReturn(false); - + public void testSearchUsers_InvalidOffset() { // Act & Assert - mockMvc.perform(get("/openapi/v1/users")) - .andExpect(status().isForbidden()); + assertThrows(BadRequestException.class, + () -> userController.searchUsers("test", false, -1, 10)); verify(userService, times(0)).searchUsers(anyString(), anyInt(), anyInt(), anyBoolean()); } From d0f1eccee71cbdf320158bb350a2d10c6d44c2ad Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Thu, 19 Feb 2026 16:36:40 +0800 Subject: [PATCH 05/12] feat: implement create user role and permissions in consumer management --- .../auth/ConsumerPermissionValidator.java | 12 ++++ .../openapi/service/ConsumerService.java | 62 ++++++++++++++++++- .../openapi/v1/controller/UserController.java | 6 +- .../portal/component/PermissionValidator.java | 4 ++ .../component/UserPermissionValidator.java | 11 ++++ .../portal/component/config/PortalConfig.java | 12 +++- .../portal/constant/PermissionType.java | 1 + .../portal/controller/ConsumerController.java | 3 + .../controller/PermissionController.java | 46 ++++++++++++++ .../vo/consumer/ConsumerCreateRequestVO.java | 10 +++ .../entity/vo/consumer/ConsumerInfo.java | 10 +++ .../service/RoleInitializationService.java | 2 + .../service/SystemRoleManagerService.java | 20 ++++++ .../DefaultRoleInitializationService.java | 26 +++++++- 14 files changed, 214 insertions(+), 11 deletions(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/auth/ConsumerPermissionValidator.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/auth/ConsumerPermissionValidator.java index 49f06a20335..e7641012d48 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/auth/ConsumerPermissionValidator.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/auth/ConsumerPermissionValidator.java @@ -94,6 +94,18 @@ public boolean hasManageAppMasterPermission(String appId) { throw new UnsupportedOperationException("Not supported operation"); } + @Override + public boolean hasCreateUserPermission() { + long consumerId = consumerAuthUtil.retrieveConsumerIdFromCtx(); + return permissionService.consumerHasPermission(consumerId, PermissionType.CREATE_USER, + SYSTEM_PERMISSION_TARGET_ID); + } + + @Override + public boolean hasCreateUserPermission(String userId) { + return false; + } + @Override protected boolean hasPermissions(List requiredPerms) { if (requiredPerms == null || requiredPerms.isEmpty()) { diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java index a1d17d75872..e43b42b7370 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java @@ -206,7 +206,7 @@ public List assignNamespaceRoleToConsumer(String token, String app } private ConsumerInfo convert(Consumer consumer, String token, boolean allowCreateApplication, - Integer rateLimit) { + boolean allowCreateUser, Integer rateLimit) { ConsumerInfo consumerInfo = new ConsumerInfo(); consumerInfo.setConsumerId(consumer.getId()); consumerInfo.setAppId(consumer.getAppId()); @@ -219,6 +219,7 @@ private ConsumerInfo convert(Consumer consumer, String token, boolean allowCreat consumerInfo.setToken(token); consumerInfo.setAllowCreateApplication(allowCreateApplication); + consumerInfo.setAllowCreateUser(allowCreateUser); return consumerInfo; } @@ -231,7 +232,9 @@ public ConsumerInfo getConsumerInfoByAppId(String appId) { if (consumer == null) { return null; } - return convert(consumer, consumerToken.getToken(), isAllowCreateApplication(consumer.getId()), + return convert(consumer, consumerToken.getToken(), + isAllowCreateApplication(consumer.getId()), + isAllowCreateUser(consumer.getId()), getRateLimit(consumer.getId())); } @@ -239,6 +242,10 @@ private boolean isAllowCreateApplication(Long consumerId) { return isAllowCreateApplication(Collections.singletonList(consumerId)).get(0); } + private boolean isAllowCreateUser(Long consumerId) { + return isAllowCreateUser(Collections.singletonList(consumerId)).get(0); + } + private Integer getRateLimit(Long consumerId) { List list = getRateLimit(Collections.singletonList(consumerId)); if (CollectionUtils.isEmpty(list)) { @@ -268,6 +275,27 @@ private List isAllowCreateApplication(List consumerIdList) { return list; } + private List isAllowCreateUser(List consumerIdList) { + Role createUserRole = getCreateUserRole(); + if (createUserRole == null) { + List list = new ArrayList<>(consumerIdList.size()); + for (Long ignored : consumerIdList) { + list.add(false); + } + return list; + } + + long roleId = createUserRole.getId(); + List list = new ArrayList<>(consumerIdList.size()); + for (Long consumerId : consumerIdList) { + ConsumerRole createUserConsumerRole = + consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, roleId); + list.add(createUserConsumerRole != null); + } + + return list; + } + private List getRateLimit(List consumerIds) { List consumerTokens = consumerTokenRepository.findByConsumerIdIn(consumerIds); Map consumerRateLimits = consumerTokens.stream().collect(Collectors.toMap( @@ -282,6 +310,10 @@ private Role getCreateAppRole() { return rolePermissionService.findRoleByRoleName(CREATE_APPLICATION_ROLE_NAME); } + private Role getCreateUserRole() { + return rolePermissionService.findRoleByRoleName(SystemRoleManagerService.CREATE_USER_ROLE_NAME); + } + public ConsumerRole assignCreateApplicationRoleToConsumer(String token) { Long consumerId = getConsumerIdByToken(token); if (consumerId == null) { @@ -304,6 +336,28 @@ public ConsumerRole assignCreateApplicationRoleToConsumer(String token) { return consumerRoleRepository.save(consumerRole); } + public ConsumerRole assignCreateUserRoleToConsumer(String token) { + Long consumerId = getConsumerIdByToken(token); + if (consumerId == null) { + throw new BadRequestException("Token is Illegal"); + } + Role createUserRole = getCreateUserRole(); + if (createUserRole == null) { + throw NotFoundException.roleNotFound(SystemRoleManagerService.CREATE_USER_ROLE_NAME); + } + + long roleId = createUserRole.getId(); + ConsumerRole createUserConsumerRole = + consumerRoleRepository.findByConsumerIdAndRoleId(consumerId, roleId); + if (createUserConsumerRole != null) { + return createUserConsumerRole; + } + + String operator = userInfoHolder.getUser().getUserId(); + ConsumerRole consumerRole = createConsumerRole(consumerId, roleId, operator); + return consumerRoleRepository.save(consumerRole); + } + @Transactional public ConsumerRole assignAppRoleToConsumer(String token, String appId) { @@ -436,6 +490,7 @@ public List findConsumerInfoList(Pageable page) { List consumerIdList = consumerList.stream().map(Consumer::getId).collect(Collectors.toList()); List allowCreateApplicationList = isAllowCreateApplication(consumerIdList); + List allowCreateUserList = isAllowCreateUser(consumerIdList); List rateLimitList = getRateLimit(consumerIdList); List consumerInfoList = new ArrayList<>(consumerList.size()); @@ -444,7 +499,8 @@ public List findConsumerInfoList(Pageable page) { Consumer consumer = consumerList.get(i); // without token ConsumerInfo consumerInfo = - convert(consumer, null, allowCreateApplicationList.get(i), rateLimitList.get(i)); + convert(consumer, null, allowCreateApplicationList.get(i), + allowCreateUserList.get(i), rateLimitList.get(i)); consumerInfoList.add(consumerInfo); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index d420da0db75..008d3b99229 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -66,7 +66,7 @@ public UserController(final UserService userService, * @param openUserDTO user information to create * @return ResponseEntity with created user information */ - @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") + @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateUserPermission()") @ApolloAuditLog(name = "OpenAPI.createUser", type = OpType.CREATE, description = "Create user via OpenAPI") @PostMapping("/users") @@ -117,7 +117,7 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) * @param userId the user ID to query * @return UserInfo object */ - @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") + @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateUserPermission()") @GetMapping("/users/{userId}") public ResponseEntity getUserByUserId(@PathVariable String userId) { UserInfo userInfo = userService.findByUserId(userId); @@ -136,7 +136,7 @@ public ResponseEntity getUserByUserId(@PathVariable String userId) { * @param limit pagination limit * @return list of UserInfo objects */ - @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") + @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateUserPermission()") @GetMapping("/users") public ResponseEntity> searchUsers( @RequestParam(value = "keyword", required = false, defaultValue = "") String keyword, diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/PermissionValidator.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/PermissionValidator.java index 00ff87b773c..133e4484611 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/PermissionValidator.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/PermissionValidator.java @@ -58,4 +58,8 @@ boolean shouldHideConfigToCurrentUser(String appId, String env, String clusterNa boolean hasCreateApplicationPermission(String userId); boolean hasManageAppMasterPermission(String appId); + + boolean hasCreateUserPermission(); + + boolean hasCreateUserPermission(String userId); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java index f908a41c18e..e39319bca95 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java @@ -104,6 +104,17 @@ public boolean hasManageAppMasterPermission(String appId) { .hasManageAppMasterPermission(userInfoHolder.getUser().getUserId(), appId)); } + @Override + public boolean hasCreateUserPermission() { + return systemRoleManagerService + .hasCreateUserPermission(userInfoHolder.getUser().getUserId()); + } + + @Override + public boolean hasCreateUserPermission(String userId) { + return systemRoleManagerService.hasCreateUserPermission(userId); + } + @Override protected boolean hasPermissions(List requiredPerms) { if (requiredPerms == null || requiredPerms.isEmpty()) { diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java index 3e697923be3..e17cf85793a 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java @@ -49,7 +49,8 @@ public class PortalConfig extends RefreshableConfig { 10; // 10s private static final Gson GSON = new Gson(); - private static final Type ORGANIZATION = new TypeToken>() {}.getType(); + private static final Type ORGANIZATION = new TypeToken>() { + }.getType(); private static final List DEFAULT_USER_PASSWORD_NOT_ALLOW_LIST = Arrays.asList("111", "222", "333", "444", "555", "666", "777", "888", "999", "000", "001122", "112233", "223344", @@ -61,7 +62,8 @@ public class PortalConfig extends RefreshableConfig { /** * meta servers config in "PortalDB.ServerConfig" */ - private static final Type META_SERVERS = new TypeToken>() {}.getType(); + private static final Type META_SERVERS = new TypeToken>() { + }.getType(); private final PortalDBPropertySource portalDBPropertySource; @@ -79,7 +81,7 @@ public List getRefreshablePropertySources() { **/ public List portalSupportedEnvs() { String[] configurations = - getArrayProperty("apollo.portal.envs", new String[] {"FAT", "UAT", "PRO"}); + getArrayProperty("apollo.portal.envs", new String[]{"FAT", "UAT", "PRO"}); List envs = Lists.newLinkedList(); for (String env : configurations) { @@ -307,6 +309,10 @@ public boolean isManageAppMasterPermissionEnabled() { return getBooleanProperty(SystemRoleManagerService.MANAGE_APP_MASTER_LIMIT_SWITCH_KEY, false); } + public boolean isCreateUserPermissionEnabled() { + return getBooleanProperty(SystemRoleManagerService.CREATE_USER_LIMIT_SWITCH_KEY, false); + } + public String getAdminServiceAccessTokens() { return getValue("admin-service.access.tokens"); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/PermissionType.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/PermissionType.java index a38e2f92b4c..b4d6aae0bc5 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/PermissionType.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/constant/PermissionType.java @@ -23,6 +23,7 @@ public interface PermissionType { */ String CREATE_APPLICATION = "CreateApplication"; String MANAGE_APP_MASTER = "ManageAppMaster"; + String CREATE_USER = "CreateUser"; /** * APP level permission diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java index d273ebf5415..3972b64b0f3 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java @@ -99,6 +99,9 @@ public ConsumerInfo create(@RequestBody ConsumerCreateRequestVO requestVO, if (requestVO.isAllowCreateApplication()) { consumerService.assignCreateApplicationRoleToConsumer(consumerToken.getToken()); } + if (requestVO.isAllowCreateUser()) { + consumerService.assignCreateUserRoleToConsumer(consumerToken.getToken()); + } return consumerService.getConsumerInfoByAppId(requestVO.getAppId()); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java index 1431874e18b..0fe642732c4 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java @@ -481,4 +481,50 @@ public JsonObject isManageAppMasterPermissionEnabled() { systemRoleManagerService.isManageAppMasterPermissionEnabled()); return rs; } + + @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") + @PostMapping("/system/role/createUser") + @ApolloAuditLog(type = OpType.CREATE, name = "Auth.addCreateUserRoleToUser") + public ResponseEntity addCreateUserRoleToUser(@RequestBody List userIds) { + userIds.forEach(this::checkUserExists); + rolePermissionService.assignRoleToUsers(SystemRoleManagerService.CREATE_USER_ROLE_NAME, + new HashSet<>(userIds), userInfoHolder.getUser().getUserId()); + return ResponseEntity.ok().build(); + } + + @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") + @DeleteMapping("/system/role/createUser/{userId}") + @ApolloAuditLog(type = OpType.DELETE, name = "Auth.deleteCreateUserRoleFromUser") + public ResponseEntity deleteCreateUserRoleFromUser(@PathVariable("userId") String userId) { + checkUserExists(userId); + Set userIds = new HashSet<>(); + userIds.add(userId); + rolePermissionService.removeRoleFromUsers(SystemRoleManagerService.CREATE_USER_ROLE_NAME, + userIds, userInfoHolder.getUser().getUserId()); + return ResponseEntity.ok().build(); + } + + @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") + @GetMapping("/system/role/createUser") + public List getCreateUserRoleUsers() { + return rolePermissionService + .queryUsersWithRole(SystemRoleManagerService.CREATE_USER_ROLE_NAME).stream() + .map(UserInfo::getUserId).collect(Collectors.toList()); + } + + @GetMapping("/system/role/createUser/{userId}") + public JsonObject hasCreateUserPermission(@PathVariable String userId) { + JsonObject rs = new JsonObject(); + rs.addProperty("hasCreateUserPermission", + unifiedPermissionValidator.hasCreateUserPermission(userId)); + return rs; + } + + @GetMapping("/system/role/createUser/enabled") + public JsonObject isCreateUserPermissionEnabled() { + JsonObject rs = new JsonObject(); + rs.addProperty("isCreateUserPermissionEnabled", + systemRoleManagerService.isCreateUserPermissionEnabled()); + return rs; + } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java index ca7e875f2b8..6f8e9c3696a 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerCreateRequestVO.java @@ -20,8 +20,10 @@ * @see com.ctrip.framework.apollo.openapi.entity.Consumer */ public class ConsumerCreateRequestVO { + private String appId; private boolean allowCreateApplication; + private boolean allowCreateUser; private String name; private String orgId; private String orgName; @@ -45,6 +47,14 @@ public void setAllowCreateApplication(boolean allowCreateApplication) { this.allowCreateApplication = allowCreateApplication; } + public boolean isAllowCreateUser() { + return allowCreateUser; + } + + public void setAllowCreateUser(boolean allowCreateUser) { + this.allowCreateUser = allowCreateUser; + } + public String getName() { return name; } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java index 64ea36d7112..477d7ea5363 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/consumer/ConsumerInfo.java @@ -21,6 +21,7 @@ * @see com.ctrip.framework.apollo.openapi.entity.ConsumerRole */ public class ConsumerInfo { + private String appId; private String name; @@ -32,6 +33,7 @@ public class ConsumerInfo { private long consumerId; private String token; private boolean allowCreateApplication; + private boolean allowCreateUser; private Integer rateLimit; @@ -107,6 +109,14 @@ public void setAllowCreateApplication(boolean allowCreateApplication) { this.allowCreateApplication = allowCreateApplication; } + public boolean isAllowCreateUser() { + return allowCreateUser; + } + + public void setAllowCreateUser(boolean allowCreateUser) { + this.allowCreateUser = allowCreateUser; + } + public Integer getRateLimit() { return rateLimit; } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/RoleInitializationService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/RoleInitializationService.java index 44e4527235c..c91e6abf468 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/RoleInitializationService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/RoleInitializationService.java @@ -31,6 +31,8 @@ void initNamespaceSpecificEnvRoles(String appId, String namespaceName, String en void initCreateAppRole(); + void initCreateUserRole(); + void initManageAppMasterRole(String appId, String operator); void initClusterNamespaceRoles(String appId, String env, String clusterName, String operator); diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java index c6eab7a3378..6bb2b6073dc 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java @@ -26,6 +26,7 @@ @Service public class SystemRoleManagerService { + public static final Logger logger = LoggerFactory.getLogger(SystemRoleManagerService.class); public static final String SYSTEM_PERMISSION_TARGET_ID = "SystemRole"; @@ -34,9 +35,14 @@ public class SystemRoleManagerService { RoleUtils.buildCreateApplicationRoleName(PermissionType.CREATE_APPLICATION, SYSTEM_PERMISSION_TARGET_ID); + public static final String CREATE_USER_ROLE_NAME = + RoleUtils.buildCreateApplicationRoleName(PermissionType.CREATE_USER, + SYSTEM_PERMISSION_TARGET_ID); + public static final String CREATE_APPLICATION_LIMIT_SWITCH_KEY = "role.create-application.enabled"; public static final String MANAGE_APP_MASTER_LIMIT_SWITCH_KEY = "role.manage-app-master.enabled"; + public static final String CREATE_USER_LIMIT_SWITCH_KEY = "role.create-user.enabled"; private final RolePermissionService rolePermissionService; @@ -54,6 +60,7 @@ public SystemRoleManagerService(final RolePermissionService rolePermissionServic @PostConstruct private void init() { roleInitializationService.initCreateAppRole(); + roleInitializationService.initCreateUserRole(); } private boolean isCreateApplicationPermissionEnabled() { @@ -64,6 +71,10 @@ public boolean isManageAppMasterPermissionEnabled() { return portalConfig.isManageAppMasterPermissionEnabled(); } + public boolean isCreateUserPermissionEnabled() { + return portalConfig.isCreateUserPermissionEnabled(); + } + public boolean hasCreateApplicationPermission(String userId) { if (!isCreateApplicationPermissionEnabled()) { return true; @@ -80,4 +91,13 @@ public boolean hasManageAppMasterPermission(String userId, String appId) { return rolePermissionService.userHasPermission(userId, PermissionType.MANAGE_APP_MASTER, appId); } + + public boolean hasCreateUserPermission(String userId) { + if (!isCreateUserPermissionEnabled()) { + return true; + } + + return rolePermissionService.userHasPermission(userId, PermissionType.CREATE_USER, + SYSTEM_PERMISSION_TARGET_ID); + } } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java index 3bcffa70e2d..cee77563e07 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java @@ -81,10 +81,10 @@ public void initAppRoles(App app) { // assign modify、release namespace role to user rolePermissionService.assignRoleToUsers(RoleUtils.buildNamespaceRoleName(appId, - ConfigConsts.NAMESPACE_APPLICATION, RoleType.MODIFY_NAMESPACE), + ConfigConsts.NAMESPACE_APPLICATION, RoleType.MODIFY_NAMESPACE), Sets.newHashSet(app.getOwnerName()), operator); rolePermissionService.assignRoleToUsers(RoleUtils.buildNamespaceRoleName(appId, - ConfigConsts.NAMESPACE_APPLICATION, RoleType.RELEASE_NAMESPACE), + ConfigConsts.NAMESPACE_APPLICATION, RoleType.RELEASE_NAMESPACE), Sets.newHashSet(app.getOwnerName()), operator); } @@ -157,6 +157,28 @@ public void initCreateAppRole() { Sets.newHashSet(createAppPermission.getId())); } + @Transactional + @Override + public void initCreateUserRole() { + if (rolePermissionService + .findRoleByRoleName(SystemRoleManagerService.CREATE_USER_ROLE_NAME) != null) { + return; + } + Permission createUserPermission = permissionRepository.findTopByPermissionTypeAndTargetId( + PermissionType.CREATE_USER, SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID); + if (createUserPermission == null) { + // create user permission init + createUserPermission = createPermission(SystemRoleManagerService.SYSTEM_PERMISSION_TARGET_ID, + PermissionType.CREATE_USER, "apollo"); + rolePermissionService.createPermission(createUserPermission); + } + // create user role init + Role createUserRole = + createRole(SystemRoleManagerService.CREATE_USER_ROLE_NAME, "apollo"); + rolePermissionService.createRoleWithPermissions(createUserRole, + Sets.newHashSet(createUserPermission.getId())); + } + @Transactional public void createManageAppMasterRole(String appId, String operator) { Permission permission = createPermission(appId, PermissionType.MANAGE_APP_MASTER, operator); From d44c22c6b898f7766f7cf612c5b900dee9866d17 Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Fri, 20 Feb 2026 10:52:37 +0800 Subject: [PATCH 06/12] feat: add create user permission checks in permission validators --- .../openapi/service/ConsumerService.java | 1 + .../component/UnifiedPermissionValidator.java | 10 +++++++++ .../AbstractPermissionValidatorTest.java | 22 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java index e43b42b7370..de51ababa92 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java @@ -19,6 +19,7 @@ import static com.ctrip.framework.apollo.portal.service.SystemRoleManagerService.CREATE_APPLICATION_ROLE_NAME; import com.ctrip.framework.apollo.common.exception.BadRequestException; +import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.ctrip.framework.apollo.common.exception.NotFoundException; import com.ctrip.framework.apollo.openapi.entity.Consumer; import com.ctrip.framework.apollo.openapi.entity.ConsumerAudit; diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UnifiedPermissionValidator.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UnifiedPermissionValidator.java index 28b3e157339..49dc22f730d 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UnifiedPermissionValidator.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UnifiedPermissionValidator.java @@ -106,4 +106,14 @@ public boolean hasDeleteNamespacePermission(String appId) { public boolean hasManageAppMasterPermission(String appId) { return getDelegate().hasManageAppMasterPermission(appId); } + + @Override + public boolean hasCreateUserPermission() { + return getDelegate().hasCreateUserPermission(); + } + + @Override + public boolean hasCreateUserPermission(String userId) { + return getDelegate().hasCreateUserPermission(userId); + } } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/AbstractPermissionValidatorTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/AbstractPermissionValidatorTest.java index 790a9dd04ed..152fa684cc8 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/AbstractPermissionValidatorTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/component/AbstractPermissionValidatorTest.java @@ -205,6 +205,7 @@ public void testHasManageAppMasterPermission_WhenWithPermission() { } private static class AbstractPermissionValidatorImpl extends AbstractPermissionValidator { + @Override public boolean hasCreateAppNamespacePermission(String appId, AppNamespace appNamespace) { return false; @@ -234,10 +235,21 @@ protected boolean hasPermissions(List requiredPerms) { public boolean hasCreateApplicationPermission(String userId) { return false; } + + @Override + public boolean hasCreateUserPermission() { + return false; + } + + @Override + public boolean hasCreateUserPermission(String userId) { + return false; + } } private static class AbstractPermissionValidatorWithPermissionsImpl extends AbstractPermissionValidator { + private final List allowed; AbstractPermissionValidatorWithPermissionsImpl(List allowed) { @@ -273,5 +285,15 @@ protected boolean hasPermissions(List requiredPerms) { public boolean hasCreateApplicationPermission(String userId) { return true; } + + @Override + public boolean hasCreateUserPermission() { + return true; + } + + @Override + public boolean hasCreateUserPermission(String userId) { + return true; + } } } From e918b53fcefa9efa838bb7404b55daf3e0325dbf Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Fri, 20 Feb 2026 10:52:48 +0800 Subject: [PATCH 07/12] test: add unit tests for consumer role assignment and user creation permissions --- .../openapi/service/ConsumerServiceTest.java | 155 ++++++++++++++++++ .../controller/ConsumerControllerTest.java | 90 +++++++++- 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java index 04fcc02c156..af548d93425 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java @@ -16,6 +16,9 @@ */ package com.ctrip.framework.apollo.openapi.service; +import com.ctrip.framework.apollo.common.exception.BadRequestException; +import com.ctrip.framework.apollo.common.exception.NotFoundException; +import com.ctrip.framework.apollo.portal.service.SystemRoleManagerService; import com.ctrip.framework.apollo.openapi.entity.Consumer; import com.ctrip.framework.apollo.openapi.entity.ConsumerRole; import com.ctrip.framework.apollo.openapi.entity.ConsumerToken; @@ -51,6 +54,7 @@ @SpringBootTest @ContextConfiguration(classes = ConsumerService.class) public class ConsumerServiceTest { + @SpyBean private ConsumerService consumerService; @MockBean @@ -304,6 +308,157 @@ void allowCreateApplication() { assertEquals(consumerId, consumerInfo.getConsumerId()); } + + @Test + void notAllowCreateUser() { + final String appId = "appId-consumer-user-001"; + final String token = "token-user-001"; + final long consumerId = 1001L; + + Consumer consumer = new Consumer(); + consumer.setAppId(appId); + consumer.setId(consumerId); + when(consumerRepository.findByAppId(eq(appId))).thenReturn(consumer); + + ConsumerToken consumerToken = new ConsumerToken(); + consumerToken.setToken(token); + consumerToken.setRateLimit(0); + when(consumerTokenRepository.findByConsumerId(eq(consumerId))).thenReturn(consumerToken); + + // getCreateUserRole 返回 null,即 CREATE_USER 角色不存在 + when(rolePermissionService.findRoleByRoleName( + eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(null); + + ConsumerInfo consumerInfo = consumerService.getConsumerInfoByAppId(appId); + assertFalse(consumerInfo.isAllowCreateUser()); + assertEquals(appId, consumerInfo.getAppId()); + assertEquals(token, consumerInfo.getToken()); + } + + @Test + void allowCreateUser() { + final String appId = "appId-consumer-user-002"; + final String token = "token-user-002"; + final long consumerId = 1002L; + final long createUserRoleId = 9001L; + + Consumer consumer = new Consumer(); + consumer.setAppId(appId); + consumer.setId(consumerId); + when(consumerRepository.findByAppId(eq(appId))).thenReturn(consumer); + + ConsumerToken consumerToken = new ConsumerToken(); + consumerToken.setToken(token); + consumerToken.setRateLimit(0); + when(consumerTokenRepository.findByConsumerId(eq(consumerId))).thenReturn(consumerToken); + + // CREATE_USER 角色存在,且 consumer 已被分配该角色 + Role createUserRole = createRole(createUserRoleId, + SystemRoleManagerService.CREATE_USER_ROLE_NAME); + when(rolePermissionService.findRoleByRoleName( + eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(createUserRole); + + ConsumerRole consumerRole = createConsumerRole(consumerId, createUserRoleId); + when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), + eq(createUserRoleId))).thenReturn(consumerRole); + + ConsumerInfo consumerInfo = consumerService.getConsumerInfoByAppId(appId); + assertTrue(consumerInfo.isAllowCreateUser()); + assertEquals(appId, consumerInfo.getAppId()); + assertEquals(token, consumerInfo.getToken()); + assertEquals(consumerId, consumerInfo.getConsumerId()); + } + + @Test + void assignCreateUserRoleToConsumer_success() { + final String token = "token-assign-user-001"; + final long consumerId = 2001L; + final long createUserRoleId = 9002L; + final String operator = "admin"; + + // token 对应的 consumerId + doReturn(consumerId).when(consumerService).getConsumerIdByToken(token); + + // CREATE_USER 角色存在 + Role createUserRole = createRole(createUserRoleId, + SystemRoleManagerService.CREATE_USER_ROLE_NAME); + when(rolePermissionService.findRoleByRoleName( + eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(createUserRole); + + // consumer 尚未被分配该角色 + when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), + eq(createUserRoleId))).thenReturn(null); + + UserInfo userInfo = createUser(operator); + when(userInfoHolder.getUser()).thenReturn(userInfo); + + ConsumerRole newConsumerRole = createConsumerRole(consumerId, createUserRoleId); + doReturn(newConsumerRole).when(consumerService).createConsumerRole(consumerId, + createUserRoleId, operator); + when(consumerRoleRepository.save(eq(newConsumerRole))).thenReturn(newConsumerRole); + + ConsumerRole result = consumerService.assignCreateUserRoleToConsumer(token); + + assertNotNull(result); + assertEquals(consumerId, result.getConsumerId()); + assertEquals(createUserRoleId, result.getRoleId()); + verify(consumerRoleRepository, times(1)).save(eq(newConsumerRole)); + } + + @Test + void assignCreateUserRoleToConsumer_alreadyAssigned() { + final String token = "token-assign-user-002"; + final long consumerId = 2002L; + final long createUserRoleId = 9003L; + + doReturn(consumerId).when(consumerService).getConsumerIdByToken(token); + + Role createUserRole = createRole(createUserRoleId, + SystemRoleManagerService.CREATE_USER_ROLE_NAME); + when(rolePermissionService.findRoleByRoleName( + eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(createUserRole); + + // consumer 已经被分配了该角色 + ConsumerRole existingConsumerRole = createConsumerRole(consumerId, createUserRoleId); + when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), + eq(createUserRoleId))).thenReturn(existingConsumerRole); + + ConsumerRole result = consumerService.assignCreateUserRoleToConsumer(token); + + // 直接返回已有角色,不应再次 save + assertNotNull(result); + assertEquals(consumerId, result.getConsumerId()); + verify(consumerRoleRepository, never()).save(any()); + } + + @Test + void assignCreateUserRoleToConsumer_illegalToken() { + final String illegalToken = "illegal-token"; + + // token 找不到对应的 consumerId + doReturn(null).when(consumerService).getConsumerIdByToken(illegalToken); + + assertThrows(BadRequestException.class, + () -> consumerService.assignCreateUserRoleToConsumer(illegalToken)); + verify(consumerRoleRepository, never()).save(any()); + } + + @Test + void assignCreateUserRoleToConsumer_roleNotFound() { + final String token = "token-assign-user-003"; + final long consumerId = 2003L; + + doReturn(consumerId).when(consumerService).getConsumerIdByToken(token); + + // CREATE_USER 角色不存在(未初始化) + when(rolePermissionService.findRoleByRoleName( + eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(null); + + assertThrows(NotFoundException.class, + () -> consumerService.assignCreateUserRoleToConsumer(token)); + verify(consumerRoleRepository, never()).save(any()); + } + private Consumer createConsumer(String name, String appId, String ownerName) { Consumer consumer = new Consumer(); diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java index 5e5ffa37539..3946f493514 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java @@ -85,7 +85,7 @@ void createAndAssignCreateApplicationRoleToConsumer() { ConsumerToken ConsumerToken = new ConsumerToken(); ConsumerToken.setToken(token); Mockito.when( - consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) + consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(ConsumerToken); } consumerController.create(requestVO, null); @@ -97,4 +97,92 @@ void createAndAssignCreateApplicationRoleToConsumer() { .assignCreateApplicationRoleToConsumer(Mockito.eq(token)); Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); } + + @Test + void createAndAssignCreateUserRoleToConsumer() { + ConsumerService consumerService = Mockito.mock(ConsumerService.class); + ConsumerController consumerController = new ConsumerController(consumerService); + ConsumerCreateRequestVO requestVO = new ConsumerCreateRequestVO(); + requestVO.setAppId("appId1"); + requestVO.setName("app 1"); + requestVO.setOwnerName("user1"); + requestVO.setOrgId("orgId1"); + requestVO.setAllowCreateUser(true); + + final String token = "token-xxx"; + ConsumerToken consumerToken = new ConsumerToken(); + consumerToken.setToken(token); + Mockito.when( + consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(consumerToken); + + consumerController.create(requestVO, null); + + Mockito.verify(consumerService, Mockito.times(1)).createConsumer(Mockito.any()); + Mockito.verify(consumerService, Mockito.times(1)).generateAndSaveConsumerToken(Mockito.any(), + Mockito.any(), Mockito.any()); + // allowCreateUser=true 时应调用 assignCreateUserRoleToConsumer + Mockito.verify(consumerService, Mockito.times(1)) + .assignCreateUserRoleToConsumer(Mockito.eq(token)); + // allowCreateApplication=false 时不应调用 assignCreateApplicationRoleToConsumer + Mockito.verify(consumerService, Mockito.times(0)) + .assignCreateApplicationRoleToConsumer(Mockito.any()); + Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); + } + + @Test + void createAndAssignBothCreateApplicationAndCreateUserRoleToConsumer() { + ConsumerService consumerService = Mockito.mock(ConsumerService.class); + ConsumerController consumerController = new ConsumerController(consumerService); + ConsumerCreateRequestVO requestVO = new ConsumerCreateRequestVO(); + requestVO.setAppId("appId1"); + requestVO.setName("app 1"); + requestVO.setOwnerName("user1"); + requestVO.setOrgId("orgId1"); + requestVO.setAllowCreateApplication(true); + requestVO.setAllowCreateUser(true); + + final String token = "token-xxx"; + ConsumerToken consumerToken = new ConsumerToken(); + consumerToken.setToken(token); + Mockito.when( + consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(consumerToken); + + consumerController.create(requestVO, null); + + Mockito.verify(consumerService, Mockito.times(1)).createConsumer(Mockito.any()); + Mockito.verify(consumerService, Mockito.times(1)).generateAndSaveConsumerToken(Mockito.any(), + Mockito.any(), Mockito.any()); + // 两个权限都应被分配 + Mockito.verify(consumerService, Mockito.times(1)) + .assignCreateApplicationRoleToConsumer(Mockito.eq(token)); + Mockito.verify(consumerService, Mockito.times(1)) + .assignCreateUserRoleToConsumer(Mockito.eq(token)); + Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); + } + + @Test + void createWithoutAnySystemRole() { + ConsumerService consumerService = Mockito.mock(ConsumerService.class); + ConsumerController consumerController = new ConsumerController(consumerService); + ConsumerCreateRequestVO requestVO = new ConsumerCreateRequestVO(); + requestVO.setAppId("appId1"); + requestVO.setName("app 1"); + requestVO.setOwnerName("user1"); + requestVO.setOrgId("orgId1"); + // allowCreateApplication 和 allowCreateUser 均为默认 false + + consumerController.create(requestVO, null); + + Mockito.verify(consumerService, Mockito.times(1)).createConsumer(Mockito.any()); + Mockito.verify(consumerService, Mockito.times(1)).generateAndSaveConsumerToken(Mockito.any(), + Mockito.any(), Mockito.any()); + // 两个权限均不应被分配 + Mockito.verify(consumerService, Mockito.times(0)) + .assignCreateApplicationRoleToConsumer(Mockito.any()); + Mockito.verify(consumerService, Mockito.times(0)) + .assignCreateUserRoleToConsumer(Mockito.any()); + Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); + } } From ef06d0b25f321b8e8ae4be68a4aae5a7cb1ab204 Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Fri, 20 Feb 2026 12:41:19 +0800 Subject: [PATCH 08/12] feat: add user creation permission toggle in the consumer frontend interface. --- .../src/main/resources/static/i18n/en.json | 66 +-- .../src/main/resources/static/i18n/zh-CN.json | 68 +-- .../resources/static/open/add-consumer.html | 480 +++++++++--------- .../main/resources/static/open/manage.html | 320 ++++++------ 4 files changed, 490 insertions(+), 444 deletions(-) diff --git a/apollo-portal/src/main/resources/static/i18n/en.json b/apollo-portal/src/main/resources/static/i18n/en.json index 89852f73823..41cd798458a 100644 --- a/apollo-portal/src/main/resources/static/i18n/en.json +++ b/apollo-portal/src/main/resources/static/i18n/en.json @@ -210,7 +210,7 @@ "Component.Namespace.Master.Items.SortByKey": "Filter the configurations by key", "Component.Namespace.Master.Items.FilterItem": "Filter", "Component.Namespace.Master.Items.RevokeItemTips": "Revoke configuration changes", - "Component.Namespace.Master.Items.RevokeItem" :"Revoke", + "Component.Namespace.Master.Items.RevokeItem": "Revoke", "Component.Namespace.Master.Items.SyncItemTips": "Synchronize configurations among environments", "Component.Namespace.Master.Items.SyncItem": "Synchronize", "Component.Namespace.Master.Items.DiffItemTips": "Compare the configurations among environments", @@ -676,6 +676,10 @@ "Open.Manage.Consumer.AllowCreateApplicationTips": "(Allow third-party applications to create apps and grant them app administrator privileges.", "Open.Manage.Consumer.AllowCreateApplication.No": "no", "Open.Manage.Consumer.AllowCreateApplication.Yes": "yes", + "Open.Manage.Consumer.AllowCreateUser": "Allow user creation?", + "Open.Manage.Consumer.AllowCreateUserTips": "(Allow third-party applications to create users)", + "Open.Manage.Consumer.AllowCreateUser.No": "no", + "Open.Manage.Consumer.AllowCreateUser.Yes": "yes", "Open.Manage.Consumer.RateLimit.Enabled": "Whether to enable rate limit", "Open.Manage.Consumer.RateLimit.Enabled.Tips": "(After enabling this feature, when third-party applications publish configurations on Apollo, their traffic will be controlled according to the configured QPS limit)", "Open.Manage.Consumer.RateLimitValue": "Rate limiting QPS", @@ -793,25 +797,25 @@ "Config.Diff.PleaseChooseTwoCluster": "Please select at least two clusters", "Config.Diff.TextDiffMostChooseTwoCluster": "Please select at most two clusters", "ConfigExport.Title": "Config Export/Import", - "ConfigExport.Env.TitleTips" : "(The data (application, cluster and namespace) of one cluster can be migrated to another cluster by exporting and importing the configuration)", - "ConfigExport.App.TitleTips" : "(All configurations of an application in a specified environment and cluster can be migrated to another cluster by exporting and importing the configuration)", - "ConfigExport.SelectExportEnv" : "Select the environment to export", - "ConfigExport.SelectImportEnv" : "Select the environment to import", - "ConfigExport.ExportTips" : "In case of large amount of data, the export speed is slow. Please wait patiently", - "ConfigExport.ImportConflictLabel" : "How to deal with existing namespaces when importing", - "ConfigExport.IgnoreExistedNamespace" : "Ignore existing namespaces", - "ConfigExport.OverwriteExistedNamespace" : "Overwrite existing namespace", - "ConfigExport.UploadFile" : "Upload the exported file", - "ConfigExport.UploadFileTip" : "Please upload the exported compressed file", - "ConfigExport.ImportSuccess" : "Import success", - "ConfigExport.ImportingTip" : "Importing, please wait patiently. After importing, please check whether the namespace configuration is correct. If it is correct, publish the namespace to take effect", - "ConfigExport.ImportFailed" : "Import failed", - "ConfigExport.ExportFailed" : "Export failed", - "ConfigExport.NoPermissionTip" : "You are not this project's administrator. Only project administrators have the permission to export/import configurations.", - "ConfigExport.ExportSuccess" : "Exporting data. The data volume will cause slow speed. Please wait patiently", - "ConfigExport.ImportTips" : "After the import is completed, please check whether the namespace configuration is correct. After the check is correct, it needs to be published to take effect", - "ConfigExport.Export" : "Export", - "ConfigExport.Import" : "Import", + "ConfigExport.Env.TitleTips": "(The data (application, cluster and namespace) of one cluster can be migrated to another cluster by exporting and importing the configuration)", + "ConfigExport.App.TitleTips": "(All configurations of an application in a specified environment and cluster can be migrated to another cluster by exporting and importing the configuration)", + "ConfigExport.SelectExportEnv": "Select the environment to export", + "ConfigExport.SelectImportEnv": "Select the environment to import", + "ConfigExport.ExportTips": "In case of large amount of data, the export speed is slow. Please wait patiently", + "ConfigExport.ImportConflictLabel": "How to deal with existing namespaces when importing", + "ConfigExport.IgnoreExistedNamespace": "Ignore existing namespaces", + "ConfigExport.OverwriteExistedNamespace": "Overwrite existing namespace", + "ConfigExport.UploadFile": "Upload the exported file", + "ConfigExport.UploadFileTip": "Please upload the exported compressed file", + "ConfigExport.ImportSuccess": "Import success", + "ConfigExport.ImportingTip": "Importing, please wait patiently. After importing, please check whether the namespace configuration is correct. If it is correct, publish the namespace to take effect", + "ConfigExport.ImportFailed": "Import failed", + "ConfigExport.ExportFailed": "Export failed", + "ConfigExport.NoPermissionTip": "You are not this project's administrator. Only project administrators have the permission to export/import configurations.", + "ConfigExport.ExportSuccess": "Exporting data. The data volume will cause slow speed. Please wait patiently", + "ConfigExport.ImportTips": "After the import is completed, please check whether the namespace configuration is correct. After the check is correct, it needs to be published to take effect", + "ConfigExport.Export": "Export", + "ConfigExport.Import": "Import", "ConfigExport.Download": "Download", "ConfigExport.Env.Tab": "Operate By Environment", "ConfigExport.App.Tab": "Operate By Application Cluster", @@ -945,20 +949,20 @@ "Global.NameSpace": "NameSpace Name", "Global.Key": "Key", "Global.Value": "Value", - "Global.ValueSearch.Tips" : "(Fuzzy search, key can be the name or content of the configuration item, value is the value of the configuration item.)", - "Global.Operate" : "Operate", - "Global.Expand" : "Expand", - "Global.Abbreviate" : "Abbreviate", - "Global.JumpToEditPage" : "Jump to edit page", + "Global.ValueSearch.Tips": "(Fuzzy search, key can be the name or content of the configuration item, value is the value of the configuration item.)", + "Global.Operate": "Operate", + "Global.Expand": "Expand", + "Global.Abbreviate": "Abbreviate", + "Global.JumpToEditPage": "Jump to edit page", "Item.GlobalSearchByKey": "Search by Key", "Item.GlobalSearchByValue": "Search by Value", "Item.GlobalSearch": "Search", "Item.GlobalSearchSystemError": "System error, please try again or contact the system administrator", "Item.GlobalSearch.Tips": "Search hint", - "ApolloGlobalSearch.NoData" : "No data yet, please search or add", - "Paging.TotalItems.part1" : "Total of", - "Paging.TotalItems.part2" : "records", - "Paging.DisplayNumber" : "per/Page", - "Paging.PageNumberOne" : "First", - "Paging.PageNumberLast" : "Last" + "ApolloGlobalSearch.NoData": "No data yet, please search or add", + "Paging.TotalItems.part1": "Total of", + "Paging.TotalItems.part2": "records", + "Paging.DisplayNumber": "per/Page", + "Paging.PageNumberOne": "First", + "Paging.PageNumberLast": "Last" } diff --git a/apollo-portal/src/main/resources/static/i18n/zh-CN.json b/apollo-portal/src/main/resources/static/i18n/zh-CN.json index 30db78709e8..464cacceeb7 100644 --- a/apollo-portal/src/main/resources/static/i18n/zh-CN.json +++ b/apollo-portal/src/main/resources/static/i18n/zh-CN.json @@ -318,7 +318,7 @@ "Component.Publish.OpPublish": "发布", "Component.Rollback.To": "回滚到", "Component.Rollback.Tips": "此操作将会回滚到上一个发布版本,且当前版本作废,但不影响正在修改的配置。可在发布历史页面查看当前生效的版本", - "Component.RollbackTo.Tips":"此操作将会回滚到此发布版本,且当前版本作废,但不影响正在修改的配置", + "Component.RollbackTo.Tips": "此操作将会回滚到此发布版本,且当前版本作废,但不影响正在修改的配置", "Component.Rollback.ClickToView": "点击查看", "Component.Rollback.ItemType": "Type", "Component.Rollback.ItemKey": "Key", @@ -676,6 +676,10 @@ "Open.Manage.Consumer.AllowCreateApplicationTips": "(允许第三方应用创建app,并且对创建出的app,拥有应用管理员的权限)", "Open.Manage.Consumer.AllowCreateApplication.No": "否", "Open.Manage.Consumer.AllowCreateApplication.Yes": "是", + "Open.Manage.Consumer.AllowCreateUser": "允许创建用户?", + "Open.Manage.Consumer.AllowCreateUserTips": "(允许第三方应用创建用户)", + "Open.Manage.Consumer.AllowCreateUser.No": "否", + "Open.Manage.Consumer.AllowCreateUser.Yes": "是", "Open.Manage.Consumer.RateLimit.Enabled": "是否启用限流", "Open.Manage.Consumer.RateLimit.Enabled.Tips": "(开启后,第三方应用在 Apollo 上发布配置时,会根据配置的 QPS 限制,控制其流量)", "Open.Manage.Consumer.RateLimitValue": "限流QPS", @@ -793,25 +797,25 @@ "Config.Diff.PleaseChooseTwoCluster": "请至少选择两个集群", "Config.Diff.TextDiffMostChooseTwoCluster": "文本比对至多选择两个集群", "ConfigExport.Title": "配置导出导入", - "ConfigExport.Env.TitleTips" : "(通过导出导入配置,把一个集群的数据(应用、集群、Namespace)迁移到另外一个集群)", - "ConfigExport.App.TitleTips" : "(通过导出导入配置,把一个应用在指定环境和集群下的所有配置迁移到另外一个集群)", - "ConfigExport.SelectExportEnv" : "选择导出的环境", - "ConfigExport.SelectImportEnv" : "选择导入的环境", - "ConfigExport.ImportConflictLabel" : "导入时该如何处理已存在的 Namespace", - "ConfigExport.ExportSuccess" : "正在导出数据,数据量大会导致速度慢,请耐心等待", - "ConfigExport.ExportTips" : "数据量大的情况下,导出速度较慢请耐心等待", - "ConfigExport.IgnoreExistedNamespace" : "跳过已存在的 Namespace", - "ConfigExport.OverwriteExistedNamespace" : "覆盖已存在的 Namespace", - "ConfigExport.UploadFile" : "上传导出的文件", - "ConfigExport.UploadFileTip" : "请上传导出的压缩文件", - "ConfigExport.ImportSuccess" : "导入成功", - "ConfigExport.ImportingTip" : "正在导入,请耐心等待。导入完成后,请检查 Namespace 的配置是否正确,如果无误再发布 Namespace", - "ConfigExport.ImportFailed" : "导入失败", - "ConfigExport.ExportFailed" : "导出失败", - "ConfigExport.NoPermissionTip" : "您不是应用管理员, 只有应用管理员才有导出/导入配置的权限", - "ConfigExport.Export" : "导出", - "ConfigExport.Import" : "导入", - "ConfigExport.ImportTips" : "导入完成之后,请检查 Namespace 的配置是否正确,检查无误后需要发布才能生效", + "ConfigExport.Env.TitleTips": "(通过导出导入配置,把一个集群的数据(应用、集群、Namespace)迁移到另外一个集群)", + "ConfigExport.App.TitleTips": "(通过导出导入配置,把一个应用在指定环境和集群下的所有配置迁移到另外一个集群)", + "ConfigExport.SelectExportEnv": "选择导出的环境", + "ConfigExport.SelectImportEnv": "选择导入的环境", + "ConfigExport.ImportConflictLabel": "导入时该如何处理已存在的 Namespace", + "ConfigExport.ExportSuccess": "正在导出数据,数据量大会导致速度慢,请耐心等待", + "ConfigExport.ExportTips": "数据量大的情况下,导出速度较慢请耐心等待", + "ConfigExport.IgnoreExistedNamespace": "跳过已存在的 Namespace", + "ConfigExport.OverwriteExistedNamespace": "覆盖已存在的 Namespace", + "ConfigExport.UploadFile": "上传导出的文件", + "ConfigExport.UploadFileTip": "请上传导出的压缩文件", + "ConfigExport.ImportSuccess": "导入成功", + "ConfigExport.ImportingTip": "正在导入,请耐心等待。导入完成后,请检查 Namespace 的配置是否正确,如果无误再发布 Namespace", + "ConfigExport.ImportFailed": "导入失败", + "ConfigExport.ExportFailed": "导出失败", + "ConfigExport.NoPermissionTip": "您不是应用管理员, 只有应用管理员才有导出/导入配置的权限", + "ConfigExport.Export": "导出", + "ConfigExport.Import": "导入", + "ConfigExport.ImportTips": "导入完成之后,请检查 Namespace 的配置是否正确,检查无误后需要发布才能生效", "ConfigExport.Download": "下载", "ConfigExport.Env.Tab": "按环境操作", "ConfigExport.App.Tab": "按应用集群操作", @@ -915,7 +919,7 @@ "ApolloAuditLog.DataInfluence.FieldName": "实体属性名", "ApolloAuditLog.DataInfluence.FieldNewValue": "属性值记录", "ApolloAuditLog.DataInfluence.Fields": "变化字段", - "ApolloAuditLog.DataInfluence.MatchedFields": "匹配字段", + "ApolloAuditLog.DataInfluence.MatchedFields": "匹配字段", "ApolloAuditLog.DataInfluence.HappenedTime": "记录时间", "ApolloAuditLog.TraceIdTips": "链路唯一ID", "ApolloAuditLog.OpName": "操作名称", @@ -945,20 +949,20 @@ "Global.NameSpace": "命名空间", "Global.Key": "Key", "Global.Value": "Value", - "Global.ValueSearch.Tips" : "(模糊搜索,key可为配置项名称或content,value为配置项值)", - "Global.Operate" : "操作", - "Global.Expand" : "展开", - "Global.Abbreviate" : "缩略", - "Global.JumpToEditPage" : "跳转到编辑页面", + "Global.ValueSearch.Tips": "(模糊搜索,key可为配置项名称或content,value为配置项值)", + "Global.Operate": "操作", + "Global.Expand": "展开", + "Global.Abbreviate": "缩略", + "Global.JumpToEditPage": "跳转到编辑页面", "Item.GlobalSearchByKey": "按照Key值检索", "Item.GlobalSearchByValue": "按照Value值检索", "Item.GlobalSearch": "查询", "Item.GlobalSearchSystemError": "系统出错,请重试或联系系统负责人", "Item.GlobalSearch.Tips": "搜索提示", - "ApolloGlobalSearch.NoData" : "暂无数据,请进行检索或者添加", - "Paging.TotalItems.part1" : "共", - "Paging.TotalItems.part2" : "条记录", - "Paging.DisplayNumber" : "条/页", - "Paging.PageNumberOne" : "首页", - "Paging.PageNumberLast" : "尾页" + "ApolloGlobalSearch.NoData": "暂无数据,请进行检索或者添加", + "Paging.TotalItems.part1": "共", + "Paging.TotalItems.part2": "条记录", + "Paging.DisplayNumber": "条/页", + "Paging.PageNumberOne": "首页", + "Paging.PageNumberLast": "尾页" } diff --git a/apollo-portal/src/main/resources/static/open/add-consumer.html b/apollo-portal/src/main/resources/static/open/add-consumer.html index 94c490c2b35..1d5c83863b9 100644 --- a/apollo-portal/src/main/resources/static/open/add-consumer.html +++ b/apollo-portal/src/main/resources/static/open/add-consumer.html @@ -18,265 +18,283 @@ - - - - - - - - - {{'Open.Manage.Title' | translate }} + + + + + + + + + {{'Open.Manage.Title' | translate }} - + -
-
+
+
-
- -
-
{{'Open.Manage.CreateThirdApp' | translate }} - - {{'Open.Manage.CreateThirdAppTips' | translate }} - -
-
-
-
- -
- - {{'Open.Manage.ThirdAppIdTips' | translate }} -
-
- -
-
-

-
-
+
+ +
+
{{'Open.Manage.CreateThirdApp' | translate }} + + {{'Open.Manage.CreateThirdAppTips' | translate }} + +
+
+ +
+ +
+ + {{'Open.Manage.ThirdAppIdTips' | translate }} +
+
+ +
+
+

+
+
-
- -
- - {{'Open.Manage.Consumer.AllowCreateApplicationTips' | translate }} -
-
+
+ +
+ + {{'Open.Manage.Consumer.AllowCreateApplicationTips' | translate }} +
+
-
- -
- - {{ 'Open.Manage.Consumer.RateLimit.Enabled.Tips' | translate }} -
-
+
+ +
+ + {{'Open.Manage.Consumer.AllowCreateUserTips' | translate }} +
+
-
- -
- - {{'Open.Manage.Consumer.RateLimitValueTips' | translate }} -
-
+
+ +
+ + {{ 'Open.Manage.Consumer.RateLimit.Enabled.Tips' | translate }} +
+
-
- -
- -
-
-
- -
- - {{'Open.Manage.ThirdAppNameTips' | translate }} -
-
-
- -
- -
-
+
+ +
+ + {{'Open.Manage.Consumer.RateLimitValueTips' | translate }} +
+
-
-
- -
-
- -
+
+ +
+ +
+
+
+ +
+ + {{'Open.Manage.ThirdAppNameTips' | translate }} +
+
+
+ +
+ +
+
-
-
{{'Open.Manage.GrantPermission' | translate }} - - {{'Open.Manage.GrantPermissionTips' | translate }} - -
-
-
+
+
+ +
+
+
+
-
- -
- -
-
-
- -
- -
-
-
- -
- - {{'Open.Manage.ManagedNamespaceTips' | translate }} -
-
-
- -
- - -
-
-
- -
-
- -
- {{'Open.Manage.GrantEnvTips' | translate }} -
-
-
-
- -
-
- +
+
{{'Open.Manage.GrantPermission' | translate }} + + {{'Open.Manage.GrantPermissionTips' | translate }} + +
+
+
-
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + {{'Open.Manage.ManagedNamespaceTips' | translate }} +
+
+
+ +
+ + +
+
+
+ +
+
+ +
+ {{'Open.Manage.GrantEnvTips' | translate }} +
+
+
+
+ +
+
+ -
+
-
-

{{'Common.IsRootUser' | translate }}

-
+
-
-
+
+

{{'Common.IsRootUser' | translate }}

+
-
+
+
- - +
- - - - - - - + + - - - - - - + + + + + + + - - + + + + + + - + + - - - - - - - - - - - + - + + + + + + + + + + + - - - + - + + + + + diff --git a/apollo-portal/src/main/resources/static/open/manage.html b/apollo-portal/src/main/resources/static/open/manage.html index dfe0db042fd..7b74e2e84be 100644 --- a/apollo-portal/src/main/resources/static/open/manage.html +++ b/apollo-portal/src/main/resources/static/open/manage.html @@ -18,161 +18,181 @@ - - - - - - - - - - {{'Open.Manage.Title' | translate }} + + + + + + + + + + {{'Open.Manage.Title' | translate }} - - -
-
- -
- -
-
-
-
{{'Open.Manage.CreateThirdApp' | translate }} - - {{'Open.Manage.CreateThirdAppTips' | translate }} - -
-
- -
-
- - - - - - - - - - - - - - - - - - - - - -
{{'Common.AppId' | translate }}{{'Common.AppName' | translate }}{{'Open.Manage.Consumer.AllowCreateApplication' | translate }}{{'Open.Manage.Consumer.RateLimitValue' | translate }}{{'Common.Department' | translate }}{{'Common.AppOwner' | translate }}/{{'Common.Email' | translate }}{{'Common.Operation' | translate}}
{{ consumer.appId }}{{ consumer.name }} -
- {{'Open.Manage.Consumer.AllowCreateApplication.Yes' | translate}} -
-
- {{'Open.Manage.Consumer.AllowCreateApplication.No' | translate}} -
-
- {{ consumer.rateLimit && consumer.rateLimit > 0 ? consumer.rateLimit : 'Open.Manage.Consumer.RateLimitValue.Display' | translate }} - {{ consumer.orgName + '(' + consumer.orgId + ')' }}{{ consumer.ownerName }}/{{ consumer.ownerEmail }} - - - - -
-
-
-
-
-
-
{{'Index.LoadMore' | translate }}
-
-
- - - - - -
- -
- -
-

{{'Common.IsRootUser' | translate }}

-
- + + +
+
+ +
+ +
+
+
+
{{'Open.Manage.CreateThirdApp' | translate }} + + {{'Open.Manage.CreateThirdAppTips' | translate }} + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
{{'Common.AppId' | translate }}{{'Common.AppName' | translate }}{{'Open.Manage.Consumer.AllowCreateApplication' | translate }} + {{'Open.Manage.Consumer.AllowCreateUser' | translate }}{{'Open.Manage.Consumer.RateLimitValue' | translate }}{{'Common.Department' | translate }}{{'Common.AppOwner' | translate }}/{{'Common.Email' | translate + }} + {{'Common.Operation' | translate}}
{{ consumer.appId }}{{ consumer.name }} +
+ {{'Open.Manage.Consumer.AllowCreateApplication.Yes' | translate}} +
+
+ {{'Open.Manage.Consumer.AllowCreateApplication.No' | translate}} +
+
+
+ {{'Open.Manage.Consumer.AllowCreateUser.Yes' | translate}} +
+
+ {{'Open.Manage.Consumer.AllowCreateUser.No' | translate}} +
+
+ {{ consumer.rateLimit && consumer.rateLimit > 0 ? consumer.rateLimit : + 'Open.Manage.Consumer.RateLimitValue.Display' | translate }} + {{ consumer.orgName + '(' + consumer.orgId + ')' }}{{ consumer.ownerName }}/{{ consumer.ownerEmail }} + + + + +
+
+
+
+
+
+
{{'Index.LoadMore' | translate }}
+
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + +
+

{{'Common.IsRootUser' | translate }}

+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e870483f1adacd4a7036f3488077a5138c71929e Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Fri, 20 Feb 2026 16:01:16 +0800 Subject: [PATCH 09/12] feat: update user creation response to return location URI --- .../apollo/openapi/v1/controller/UserController.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index 008d3b99229..996a9905e6c 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -36,7 +36,9 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import java.net.URI; import java.util.List; /** @@ -108,7 +110,11 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) // Retrieve and return the created user information UserInfo createdUser = userService.findByUserId(openUserDTO.getUsername()); - return ResponseEntity.ok(createdUser); + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .replacePath("/openapi/v1/users/{userId}") + .buildAndExpand(createdUser.getUserId()) + .toUri(); + return ResponseEntity.created(location).body(createdUser); } /** From 61bdf7cfe114ce3046e3e1ae0d9cb89223a79cdb Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Fri, 20 Feb 2026 16:04:19 +0800 Subject: [PATCH 10/12] refactor: simplify method calls and improve code readability in consumer services and tests --- .../openapi/service/ConsumerService.java | 11 ++-- .../openapi/v1/controller/UserController.java | 4 +- .../component/UserPermissionValidator.java | 3 +- .../portal/component/config/PortalConfig.java | 8 ++- .../controller/PermissionController.java | 5 +- .../service/SystemRoleManagerService.java | 5 +- .../DefaultRoleInitializationService.java | 7 ++- .../openapi/service/ConsumerServiceTest.java | 51 ++++++++++--------- .../controller/ConsumerControllerTest.java | 9 ++-- 9 files changed, 47 insertions(+), 56 deletions(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java index de51ababa92..7465632849c 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java @@ -233,10 +233,8 @@ public ConsumerInfo getConsumerInfoByAppId(String appId) { if (consumer == null) { return null; } - return convert(consumer, consumerToken.getToken(), - isAllowCreateApplication(consumer.getId()), - isAllowCreateUser(consumer.getId()), - getRateLimit(consumer.getId())); + return convert(consumer, consumerToken.getToken(), isAllowCreateApplication(consumer.getId()), + isAllowCreateUser(consumer.getId()), getRateLimit(consumer.getId())); } private boolean isAllowCreateApplication(Long consumerId) { @@ -499,9 +497,8 @@ public List findConsumerInfoList(Pageable page) { for (int i = 0; i < consumerList.size(); i++) { Consumer consumer = consumerList.get(i); // without token - ConsumerInfo consumerInfo = - convert(consumer, null, allowCreateApplicationList.get(i), - allowCreateUserList.get(i), rateLimitList.get(i)); + ConsumerInfo consumerInfo = convert(consumer, null, allowCreateApplicationList.get(i), + allowCreateUserList.get(i), rateLimitList.get(i)); consumerInfoList.add(consumerInfo); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index 996a9905e6c..8a693cd276d 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -111,9 +111,7 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) // Retrieve and return the created user information UserInfo createdUser = userService.findByUserId(openUserDTO.getUsername()); URI location = ServletUriComponentsBuilder.fromCurrentRequest() - .replacePath("/openapi/v1/users/{userId}") - .buildAndExpand(createdUser.getUserId()) - .toUri(); + .replacePath("/openapi/v1/users/{userId}").buildAndExpand(createdUser.getUserId()).toUri(); return ResponseEntity.created(location).body(createdUser); } diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java index e39319bca95..0e9c2f9af59 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/UserPermissionValidator.java @@ -106,8 +106,7 @@ public boolean hasManageAppMasterPermission(String appId) { @Override public boolean hasCreateUserPermission() { - return systemRoleManagerService - .hasCreateUserPermission(userInfoHolder.getUser().getUserId()); + return systemRoleManagerService.hasCreateUserPermission(userInfoHolder.getUser().getUserId()); } @Override diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java index e17cf85793a..61b5040b746 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/component/config/PortalConfig.java @@ -49,8 +49,7 @@ public class PortalConfig extends RefreshableConfig { 10; // 10s private static final Gson GSON = new Gson(); - private static final Type ORGANIZATION = new TypeToken>() { - }.getType(); + private static final Type ORGANIZATION = new TypeToken>() {}.getType(); private static final List DEFAULT_USER_PASSWORD_NOT_ALLOW_LIST = Arrays.asList("111", "222", "333", "444", "555", "666", "777", "888", "999", "000", "001122", "112233", "223344", @@ -62,8 +61,7 @@ public class PortalConfig extends RefreshableConfig { /** * meta servers config in "PortalDB.ServerConfig" */ - private static final Type META_SERVERS = new TypeToken>() { - }.getType(); + private static final Type META_SERVERS = new TypeToken>() {}.getType(); private final PortalDBPropertySource portalDBPropertySource; @@ -81,7 +79,7 @@ public List getRefreshablePropertySources() { **/ public List portalSupportedEnvs() { String[] configurations = - getArrayProperty("apollo.portal.envs", new String[]{"FAT", "UAT", "PRO"}); + getArrayProperty("apollo.portal.envs", new String[] {"FAT", "UAT", "PRO"}); List envs = Lists.newLinkedList(); for (String env : configurations) { diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java index 0fe642732c4..63fb6e68010 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java @@ -507,9 +507,8 @@ public ResponseEntity deleteCreateUserRoleFromUser(@PathVariable("userId") @PreAuthorize(value = "@unifiedPermissionValidator.isSuperAdmin()") @GetMapping("/system/role/createUser") public List getCreateUserRoleUsers() { - return rolePermissionService - .queryUsersWithRole(SystemRoleManagerService.CREATE_USER_ROLE_NAME).stream() - .map(UserInfo::getUserId).collect(Collectors.toList()); + return rolePermissionService.queryUsersWithRole(SystemRoleManagerService.CREATE_USER_ROLE_NAME) + .stream().map(UserInfo::getUserId).collect(Collectors.toList()); } @GetMapping("/system/role/createUser/{userId}") diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java index 6bb2b6073dc..b642ffda0c4 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/SystemRoleManagerService.java @@ -35,9 +35,8 @@ public class SystemRoleManagerService { RoleUtils.buildCreateApplicationRoleName(PermissionType.CREATE_APPLICATION, SYSTEM_PERMISSION_TARGET_ID); - public static final String CREATE_USER_ROLE_NAME = - RoleUtils.buildCreateApplicationRoleName(PermissionType.CREATE_USER, - SYSTEM_PERMISSION_TARGET_ID); + public static final String CREATE_USER_ROLE_NAME = RoleUtils + .buildCreateApplicationRoleName(PermissionType.CREATE_USER, SYSTEM_PERMISSION_TARGET_ID); public static final String CREATE_APPLICATION_LIMIT_SWITCH_KEY = "role.create-application.enabled"; diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java index cee77563e07..570ad1bcad9 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/defaultimpl/DefaultRoleInitializationService.java @@ -81,10 +81,10 @@ public void initAppRoles(App app) { // assign modify、release namespace role to user rolePermissionService.assignRoleToUsers(RoleUtils.buildNamespaceRoleName(appId, - ConfigConsts.NAMESPACE_APPLICATION, RoleType.MODIFY_NAMESPACE), + ConfigConsts.NAMESPACE_APPLICATION, RoleType.MODIFY_NAMESPACE), Sets.newHashSet(app.getOwnerName()), operator); rolePermissionService.assignRoleToUsers(RoleUtils.buildNamespaceRoleName(appId, - ConfigConsts.NAMESPACE_APPLICATION, RoleType.RELEASE_NAMESPACE), + ConfigConsts.NAMESPACE_APPLICATION, RoleType.RELEASE_NAMESPACE), Sets.newHashSet(app.getOwnerName()), operator); } @@ -173,8 +173,7 @@ public void initCreateUserRole() { rolePermissionService.createPermission(createUserPermission); } // create user role init - Role createUserRole = - createRole(SystemRoleManagerService.CREATE_USER_ROLE_NAME, "apollo"); + Role createUserRole = createRole(SystemRoleManagerService.CREATE_USER_ROLE_NAME, "apollo"); rolePermissionService.createRoleWithPermissions(createUserRole, Sets.newHashSet(createUserPermission.getId())); } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java index af548d93425..46573d7bcd1 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/service/ConsumerServiceTest.java @@ -326,8 +326,8 @@ void notAllowCreateUser() { when(consumerTokenRepository.findByConsumerId(eq(consumerId))).thenReturn(consumerToken); // getCreateUserRole 返回 null,即 CREATE_USER 角色不存在 - when(rolePermissionService.findRoleByRoleName( - eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(null); + when(rolePermissionService + .findRoleByRoleName(eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(null); ConsumerInfo consumerInfo = consumerService.getConsumerInfoByAppId(appId); assertFalse(consumerInfo.isAllowCreateUser()); @@ -353,14 +353,15 @@ void allowCreateUser() { when(consumerTokenRepository.findByConsumerId(eq(consumerId))).thenReturn(consumerToken); // CREATE_USER 角色存在,且 consumer 已被分配该角色 - Role createUserRole = createRole(createUserRoleId, - SystemRoleManagerService.CREATE_USER_ROLE_NAME); - when(rolePermissionService.findRoleByRoleName( - eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(createUserRole); + Role createUserRole = + createRole(createUserRoleId, SystemRoleManagerService.CREATE_USER_ROLE_NAME); + when(rolePermissionService + .findRoleByRoleName(eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))) + .thenReturn(createUserRole); ConsumerRole consumerRole = createConsumerRole(consumerId, createUserRoleId); - when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), - eq(createUserRoleId))).thenReturn(consumerRole); + when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), eq(createUserRoleId))) + .thenReturn(consumerRole); ConsumerInfo consumerInfo = consumerService.getConsumerInfoByAppId(appId); assertTrue(consumerInfo.isAllowCreateUser()); @@ -380,21 +381,22 @@ void assignCreateUserRoleToConsumer_success() { doReturn(consumerId).when(consumerService).getConsumerIdByToken(token); // CREATE_USER 角色存在 - Role createUserRole = createRole(createUserRoleId, - SystemRoleManagerService.CREATE_USER_ROLE_NAME); - when(rolePermissionService.findRoleByRoleName( - eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(createUserRole); + Role createUserRole = + createRole(createUserRoleId, SystemRoleManagerService.CREATE_USER_ROLE_NAME); + when(rolePermissionService + .findRoleByRoleName(eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))) + .thenReturn(createUserRole); // consumer 尚未被分配该角色 - when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), - eq(createUserRoleId))).thenReturn(null); + when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), eq(createUserRoleId))) + .thenReturn(null); UserInfo userInfo = createUser(operator); when(userInfoHolder.getUser()).thenReturn(userInfo); ConsumerRole newConsumerRole = createConsumerRole(consumerId, createUserRoleId); - doReturn(newConsumerRole).when(consumerService).createConsumerRole(consumerId, - createUserRoleId, operator); + doReturn(newConsumerRole).when(consumerService).createConsumerRole(consumerId, createUserRoleId, + operator); when(consumerRoleRepository.save(eq(newConsumerRole))).thenReturn(newConsumerRole); ConsumerRole result = consumerService.assignCreateUserRoleToConsumer(token); @@ -413,15 +415,16 @@ void assignCreateUserRoleToConsumer_alreadyAssigned() { doReturn(consumerId).when(consumerService).getConsumerIdByToken(token); - Role createUserRole = createRole(createUserRoleId, - SystemRoleManagerService.CREATE_USER_ROLE_NAME); - when(rolePermissionService.findRoleByRoleName( - eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(createUserRole); + Role createUserRole = + createRole(createUserRoleId, SystemRoleManagerService.CREATE_USER_ROLE_NAME); + when(rolePermissionService + .findRoleByRoleName(eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))) + .thenReturn(createUserRole); // consumer 已经被分配了该角色 ConsumerRole existingConsumerRole = createConsumerRole(consumerId, createUserRoleId); - when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), - eq(createUserRoleId))).thenReturn(existingConsumerRole); + when(consumerRoleRepository.findByConsumerIdAndRoleId(eq(consumerId), eq(createUserRoleId))) + .thenReturn(existingConsumerRole); ConsumerRole result = consumerService.assignCreateUserRoleToConsumer(token); @@ -451,8 +454,8 @@ void assignCreateUserRoleToConsumer_roleNotFound() { doReturn(consumerId).when(consumerService).getConsumerIdByToken(token); // CREATE_USER 角色不存在(未初始化) - when(rolePermissionService.findRoleByRoleName( - eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(null); + when(rolePermissionService + .findRoleByRoleName(eq(SystemRoleManagerService.CREATE_USER_ROLE_NAME))).thenReturn(null); assertThrows(NotFoundException.class, () -> consumerService.assignCreateUserRoleToConsumer(token)); diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java index 3946f493514..1c86a538e6f 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/controller/ConsumerControllerTest.java @@ -85,7 +85,7 @@ void createAndAssignCreateApplicationRoleToConsumer() { ConsumerToken ConsumerToken = new ConsumerToken(); ConsumerToken.setToken(token); Mockito.when( - consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) + consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(ConsumerToken); } consumerController.create(requestVO, null); @@ -113,7 +113,7 @@ void createAndAssignCreateUserRoleToConsumer() { ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setToken(token); Mockito.when( - consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) + consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(consumerToken); consumerController.create(requestVO, null); @@ -146,7 +146,7 @@ void createAndAssignBothCreateApplicationAndCreateUserRoleToConsumer() { ConsumerToken consumerToken = new ConsumerToken(); consumerToken.setToken(token); Mockito.when( - consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) + consumerService.generateAndSaveConsumerToken(Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(consumerToken); consumerController.create(requestVO, null); @@ -181,8 +181,7 @@ void createWithoutAnySystemRole() { // 两个权限均不应被分配 Mockito.verify(consumerService, Mockito.times(0)) .assignCreateApplicationRoleToConsumer(Mockito.any()); - Mockito.verify(consumerService, Mockito.times(0)) - .assignCreateUserRoleToConsumer(Mockito.any()); + Mockito.verify(consumerService, Mockito.times(0)).assignCreateUserRoleToConsumer(Mockito.any()); Mockito.verify(consumerService, Mockito.times(1)).getConsumerInfoByAppId(Mockito.any()); } } From eadd0285c713334adb44db48713741743d8dc584 Mon Sep 17 00:00:00 2001 From: No-99-Tongji <3167937401@qq.com> Date: Fri, 20 Feb 2026 18:13:30 +0800 Subject: [PATCH 11/12] feat: update user creation response to return user information directly --- .../apollo/openapi/v1/controller/UserController.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index 8a693cd276d..008d3b99229 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -36,9 +36,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.net.URI; import java.util.List; /** @@ -110,9 +108,7 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) // Retrieve and return the created user information UserInfo createdUser = userService.findByUserId(openUserDTO.getUsername()); - URI location = ServletUriComponentsBuilder.fromCurrentRequest() - .replacePath("/openapi/v1/users/{userId}").buildAndExpand(createdUser.getUserId()).toUri(); - return ResponseEntity.created(location).body(createdUser); + return ResponseEntity.ok(createdUser); } /** From ce468f05f4d450f0b71cc317c69ff4968708bf04 Mon Sep 17 00:00:00 2001 From: "li.luo" Date: Tue, 24 Feb 2026 14:55:33 +0800 Subject: [PATCH 12/12] feat: implement OpenAPI user management methods and response model conversion --- .../openapi/v1/controller/UserController.java | 54 ++++++++++++++----- .../v1/controller/UserControllerTest.java | 53 +++++++++--------- 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java index 008d3b99229..bc46572e5f8 100644 --- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java +++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/UserController.java @@ -20,8 +20,9 @@ import com.ctrip.framework.apollo.audit.annotation.OpType; import com.ctrip.framework.apollo.common.exception.BadRequestException; import com.ctrip.framework.apollo.core.utils.StringUtils; -import com.ctrip.framework.apollo.openapi.dto.OpenUserDTO; -import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; +import com.ctrip.framework.apollo.openapi.api.UserManagementApi; +import com.ctrip.framework.apollo.openapi.model.OpenUserDTO; +import com.ctrip.framework.apollo.openapi.model.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import com.ctrip.framework.apollo.portal.spi.UserService; import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; @@ -38,6 +39,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; +import java.util.stream.Collectors; /** * OpenAPI User Management Controller Provides RESTful APIs for user management operations through @@ -47,7 +49,7 @@ */ @RestController("openapiUserController") @RequestMapping("/openapi/v1") -public class UserController { +public class UserController implements UserManagementApi { private static final int DEFAULT_USER_ENABLED = 1; @@ -60,6 +62,21 @@ public UserController(final UserService userService, this.passwordChecker = passwordChecker; } + /** + * Convert portal UserInfo BO to generated model UserInfo + */ + private UserInfo toModelUserInfo(com.ctrip.framework.apollo.portal.entity.bo.UserInfo bo) { + if (bo == null) { + return null; + } + UserInfo model = new UserInfo(); + model.setUserId(bo.getUserId()); + model.setName(bo.getName()); + model.setEmail(bo.getEmail()); + model.setEnabled(UserInfo.EnabledEnum.fromValue(bo.getEnabled())); + return model; + } + /** * Create a new user * @@ -70,6 +87,7 @@ public UserController(final UserService userService, @ApolloAuditLog(name = "OpenAPI.createUser", type = OpType.CREATE, description = "Create user via OpenAPI") @PostMapping("/users") + @Override public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) { // Validate required fields if (StringUtils.isContainEmpty(openUserDTO.getUsername(), openUserDTO.getPassword())) { @@ -100,15 +118,19 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) userPO.setUserDisplayName( openUserDTO.getUserDisplayName() != null ? openUserDTO.getUserDisplayName() : openUserDTO.getUsername()); - userPO.setEnabled( - openUserDTO.getEnabled() != null ? openUserDTO.getEnabled() : DEFAULT_USER_ENABLED); + int enabledVal = DEFAULT_USER_ENABLED; + if (openUserDTO.getEnabled() != null) { + enabledVal = openUserDTO.getEnabled().getValue(); + } + userPO.setEnabled(enabledVal); // Create user ((SpringSecurityUserService) userService).create(userPO); // Retrieve and return the created user information - UserInfo createdUser = userService.findByUserId(openUserDTO.getUsername()); - return ResponseEntity.ok(createdUser); + com.ctrip.framework.apollo.portal.entity.bo.UserInfo createdUser = + userService.findByUserId(openUserDTO.getUsername()); + return ResponseEntity.ok(toModelUserInfo(createdUser)); } /** @@ -119,12 +141,14 @@ public ResponseEntity createUser(@RequestBody OpenUserDTO openUserDTO) */ @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateUserPermission()") @GetMapping("/users/{userId}") + @Override public ResponseEntity getUserByUserId(@PathVariable String userId) { - UserInfo userInfo = userService.findByUserId(userId); + com.ctrip.framework.apollo.portal.entity.bo.UserInfo userInfo = + userService.findByUserId(userId); if (userInfo == null) { throw new BadRequestException("User not found: " + userId); } - return ResponseEntity.ok(userInfo); + return ResponseEntity.ok(toModelUserInfo(userInfo)); } /** @@ -138,12 +162,13 @@ public ResponseEntity getUserByUserId(@PathVariable String userId) { */ @PreAuthorize(value = "@unifiedPermissionValidator.hasCreateUserPermission()") @GetMapping("/users") + @Override public ResponseEntity> searchUsers( @RequestParam(value = "keyword", required = false, defaultValue = "") String keyword, @RequestParam(value = "includeInactiveUsers", - defaultValue = "false") boolean includeInactiveUsers, - @RequestParam(value = "offset", defaultValue = "0") int offset, - @RequestParam(value = "limit", defaultValue = "10") int limit) { + defaultValue = "false") Boolean includeInactiveUsers, + @RequestParam(value = "offset", defaultValue = "0") Integer offset, + @RequestParam(value = "limit", defaultValue = "10") Integer limit) { if (limit <= 0 || limit > 100) { throw new BadRequestException("Limit must be between 1 and 100"); @@ -153,7 +178,10 @@ public ResponseEntity> searchUsers( throw new BadRequestException("Offset must be non-negative"); } - List users = userService.searchUsers(keyword, offset, limit, includeInactiveUsers); + List users = userService.searchUsers(keyword, offset, limit, includeInactiveUsers) + .stream() + .map(this::toModelUserInfo) + .collect(Collectors.toList()); return ResponseEntity.ok(users); } } diff --git a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java index 153393b92f3..32fba4ea1b7 100644 --- a/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java +++ b/apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/UserControllerTest.java @@ -17,8 +17,8 @@ package com.ctrip.framework.apollo.openapi.v1.controller; import com.ctrip.framework.apollo.common.exception.BadRequestException; -import com.ctrip.framework.apollo.openapi.dto.OpenUserDTO; -import com.ctrip.framework.apollo.portal.entity.bo.UserInfo; +import com.ctrip.framework.apollo.openapi.model.OpenUserDTO; +import com.ctrip.framework.apollo.openapi.model.UserInfo; import com.ctrip.framework.apollo.portal.entity.po.UserPO; import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService; import com.ctrip.framework.apollo.portal.util.checker.AuthUserPasswordChecker; @@ -72,6 +72,18 @@ public void setUp() { userController = new UserController(userService, passwordChecker); } + // Helper to build a portal UserInfo BO (what UserService returns) + private com.ctrip.framework.apollo.portal.entity.bo.UserInfo portalUser(String userId, + String name, String email) { + com.ctrip.framework.apollo.portal.entity.bo.UserInfo u = + new com.ctrip.framework.apollo.portal.entity.bo.UserInfo(); + u.setUserId(userId); + u.setName(name); + u.setEmail(email); + u.setEnabled(1); + return u; + } + @Test public void testCreateUser_Success() { // Arrange @@ -80,17 +92,14 @@ public void testCreateUser_Success() { openUserDTO.setPassword("StrongP@ssw0rd"); openUserDTO.setEmail("testuser@example.com"); openUserDTO.setUserDisplayName("Test User"); - openUserDTO.setEnabled(1); + openUserDTO.setEnabled(OpenUserDTO.EnabledEnum.NUMBER_1); CheckResult checkResult = new CheckResult(true, ""); when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); doNothing().when(userService).create(any(UserPO.class)); - UserInfo createdUser = new UserInfo(); - createdUser.setUserId("testuser"); - createdUser.setName("Test User"); - createdUser.setEmail("testuser@example.com"); - when(userService.findByUserId("testuser")).thenReturn(createdUser); + when(userService.findByUserId("testuser")) + .thenReturn(portalUser("testuser", "Test User", "testuser@example.com")); // Act ResponseEntity response = userController.createUser(openUserDTO); @@ -125,9 +134,8 @@ public void testCreateUser_WithDefaultValues() { when(passwordChecker.checkWeakPassword(anyString())).thenReturn(checkResult); doNothing().when(userService).create(any(UserPO.class)); - UserInfo createdUser = new UserInfo(); - createdUser.setUserId("testuser2"); - when(userService.findByUserId("testuser2")).thenReturn(createdUser); + when(userService.findByUserId("testuser2")) + .thenReturn(portalUser("testuser2", "testuser2", "testuser2@example.com")); // Act ResponseEntity response = userController.createUser(openUserDTO); @@ -221,17 +229,12 @@ public void testCreateUser_UserAlreadyExists() { assertThrows(BadRequestException.class, () -> userController.createUser(openUserDTO)); } - @Test public void testGetUserByUserId_Success() { // Arrange String userId = "testuser"; - UserInfo userInfo = new UserInfo(); - userInfo.setUserId(userId); - userInfo.setName("Test User"); - userInfo.setEmail("testuser@example.com"); - - when(userService.findByUserId(userId)).thenReturn(userInfo); + when(userService.findByUserId(userId)) + .thenReturn(portalUser(userId, "Test User", "testuser@example.com")); // Act ResponseEntity response = userController.getUserByUserId(userId); @@ -258,19 +261,13 @@ public void testGetUserByUserId_UserNotFound() { verify(userService, times(1)).findByUserId(userId); } - @Test public void testSearchUsers_Success() { // Arrange - UserInfo user1 = new UserInfo(); - user1.setUserId("user1"); - user1.setName("User One"); - - UserInfo user2 = new UserInfo(); - user2.setUserId("user2"); - user2.setName("User Two"); - - List users = Arrays.asList(user1, user2); + List users = Arrays.asList( + portalUser("user1", "User One", "user1@example.com"), + portalUser("user2", "User Two", "user2@example.com") + ); when(userService.searchUsers(anyString(), anyInt(), anyInt(), anyBoolean())).thenReturn(users); // Act