From ce58c8d7c7ba1fa0d8dc9a8c038090c12ce4e097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B7=E5=86=B7?= Date: Fri, 10 Apr 2020 16:41:52 +0800 Subject: [PATCH] nacos 1.2.1 --- .../nacos/console/config/ConsoleConfig.java | 63 +++++ .../controller/PermissionController.java | 86 +++++++ .../console/controller/RoleController.java | 93 +++++++ .../console/controller/UserController.java | 232 ++++++++++++++++++ .../exception/ConsoleExceptionHandler.java | 52 ++++ .../nacos/CustomAuthenticationProvider.java | 56 +++++ .../nacos/JwtAuthenticationEntryPoint.java | 46 ++++ .../security/nacos/JwtTokenManager.java | 104 ++++++++ .../security/nacos/NacosAuthConfig.java | 146 +++++++++++ .../security/nacos/NacosAuthManager.java | 136 ++++++++++ .../nacos/roles/NacosRoleServiceImpl.java | 219 +++++++++++++++++ .../security/nacos/users/NacosUser.java | 52 ++++ .../nacos/users/NacosUserDetails.java | 73 ++++++ .../users/NacosUserDetailsServiceImpl.java | 110 +++++++++ .../static/console-fe/src/layouts/menu.js | 75 ++++++ .../PermissionsManagement/NewPermissions.js | 121 +++++++++ .../PermissionsManagement.js | 162 ++++++++++++ .../PermissionsManagement.scss | 12 + .../PermissionsManagement/index.js | 16 ++ .../src/pages/AuthorityControl/README.md | 7 + .../RolesManagement/NewRole.js | 90 +++++++ .../RolesManagement/RolesManagement.js | 130 ++++++++++ .../RolesManagement/RolesManagement.scss | 12 + .../AuthorityControl/RolesManagement/index.js | 16 ++ .../UserManagement/NewUser.js | 104 ++++++++ .../UserManagement/PasswordReset.js | 104 ++++++++ .../UserManagement/UserManagement.js | 150 +++++++++++ .../UserManagement/UserManagement.scss | 18 ++ .../AuthorityControl/UserManagement/index.js | 16 ++ .../src/pages/AuthorityControl/authority.scss | 17 ++ .../console-fe/src/reducers/authority.js | 143 +++++++++++ .../console-fe/src/reducers/configuration.js | 35 +++ .../console-fe/src/reducers/namespace.js | 39 +++ 33 files changed, 2735 insertions(+) create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/config/ConsoleConfig.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/controller/PermissionController.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/controller/RoleController.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/controller/UserController.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/exception/ConsoleExceptionHandler.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/CustomAuthenticationProvider.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtAuthenticationEntryPoint.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtTokenManager.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthConfig.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthManager.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/roles/NacosRoleServiceImpl.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUser.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetails.java create mode 100755 pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetailsServiceImpl.java create mode 100755 pigx-register/src/main/resources/static/console-fe/src/layouts/menu.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/NewPermissions.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.scss create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/index.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/README.md create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/NewRole.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.scss create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/index.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/NewUser.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/PasswordReset.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.scss create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/index.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/authority.scss create mode 100755 pigx-register/src/main/resources/static/console-fe/src/reducers/authority.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/reducers/configuration.js create mode 100755 pigx-register/src/main/resources/static/console-fe/src/reducers/namespace.js diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/config/ConsoleConfig.java b/pigx-register/src/main/java/com/alibaba/nacos/console/config/ConsoleConfig.java new file mode 100755 index 000000000..0a47b45b6 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/config/ConsoleConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.config; + + +import com.alibaba.nacos.core.code.ControllerMethodsCache; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.PropertySource; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +import javax.annotation.PostConstruct; + +/** + * @author yshen + * @author nkorange + * @since 1.2.0 + */ +@Component +@EnableScheduling +@PropertySource("/application.properties") +public class ConsoleConfig { + + @Autowired + private ControllerMethodsCache methodsCache; + + @PostConstruct + public void init() { + methodsCache.initClassMethod("com.alibaba.nacos.naming.controllers"); + methodsCache.initClassMethod("com.alibaba.nacos.console.controller"); + methodsCache.initClassMethod("com.alibaba.nacos.config.server.controller"); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOrigin("*"); + config.addAllowedHeader("*"); + config.setMaxAge(18000L); + config.addAllowedMethod("*"); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/controller/PermissionController.java b/pigx-register/src/main/java/com/alibaba/nacos/console/controller/PermissionController.java new file mode 100755 index 000000000..71339d4c5 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/controller/PermissionController.java @@ -0,0 +1,86 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.controller; + + +import com.alibaba.nacos.config.server.model.RestResult; +import com.alibaba.nacos.console.security.nacos.NacosAuthConfig; +import com.alibaba.nacos.console.security.nacos.roles.NacosRoleServiceImpl; +import com.alibaba.nacos.core.auth.ActionTypes; +import com.alibaba.nacos.core.auth.Secured; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + + +/** + * Permission operation controller + * + * @author nkorange + * @since 1.2.0 + */ +@RestController +@RequestMapping("/v1/auth/permissions") +public class PermissionController { + + @Autowired + private NacosRoleServiceImpl nacosRoleService; + + /** + * Query permissions of a role + * + * @param role the role + * @param pageNo page index + * @param pageSize page size + * @return permission of a role + */ + @GetMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "permissions", action = ActionTypes.READ) + public Object getPermissions(@RequestParam int pageNo, @RequestParam int pageSize, + @RequestParam(name = "role", defaultValue = StringUtils.EMPTY) String role) { + return nacosRoleService.getPermissionsFromDatabase(role, pageNo, pageSize); + } + + /** + * Add a permission to a role + * + * @param role the role + * @param resource the related resource + * @param action the related action + * @return ok if succeed + */ + @PostMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "permissions", action = ActionTypes.WRITE) + public Object addPermission(@RequestParam String role, @RequestParam String resource, @RequestParam String action) { + nacosRoleService.addPermission(role, resource, action); + return new RestResult<>(200, "add permission ok!"); + } + + /** + * Delete a permission from a role + * + * @param role the role + * @param resource the related resource + * @param action the related action + * @return ok if succeed + */ + @DeleteMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "permissions", action = ActionTypes.WRITE) + public Object deletePermission(@RequestParam String role, @RequestParam String resource, @RequestParam String action) { + nacosRoleService.deletePermission(role, resource, action); + return new RestResult<>(200, "delete permission ok!"); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/controller/RoleController.java b/pigx-register/src/main/java/com/alibaba/nacos/console/controller/RoleController.java new file mode 100755 index 000000000..69d720f5c --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/controller/RoleController.java @@ -0,0 +1,93 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.controller; + + +import com.alibaba.nacos.config.server.model.RestResult; +import com.alibaba.nacos.console.security.nacos.NacosAuthConfig; +import com.alibaba.nacos.console.security.nacos.roles.NacosRoleServiceImpl; +import com.alibaba.nacos.core.auth.ActionTypes; +import com.alibaba.nacos.core.auth.Secured; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * Role operation controller + * + * @author nkorange + * @since 1.2.0 + */ +@RestController +@RequestMapping("/v1/auth/roles") +public class RoleController { + + @Autowired + private NacosRoleServiceImpl roleService; + + /** + * Get roles list + * + * @param pageNo number index of page + * @param pageSize page size + * @param username optional, username of user + * @return role list + */ + @GetMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "roles", action = ActionTypes.READ) + public Object getRoles(@RequestParam int pageNo, @RequestParam int pageSize, + @RequestParam(name = "username", defaultValue = "") String username) { + return roleService.getRolesFromDatabase(username, pageNo, pageSize); + } + + /** + * Add a role to a user + *

+ * This method is used for 2 functions: + * 1. create a role and bind it to GLOBAL_ADMIN. + * 2. bind a role to an user. + * + * @param role + * @param username + * @return + */ + @PostMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "roles", action = ActionTypes.WRITE) + public Object addRole(@RequestParam String role, @RequestParam String username) { + roleService.addRole(role, username); + return new RestResult<>(200, "add role ok!"); + } + + /** + * Delete a role. If no username is specified, all users under this role are deleted + * + * @param role role + * @param username username + * @return ok if succeed + */ + @DeleteMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "roles", action = ActionTypes.WRITE) + public Object deleteRole(@RequestParam String role, + @RequestParam(name = "username", defaultValue = StringUtils.EMPTY) String username) { + if (StringUtils.isBlank(username)) { + roleService.deleteRole(role); + } else { + roleService.deleteRole(role, username); + } + return new RestResult<>(200, "delete role of user " + username + " ok!"); + } + +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/controller/UserController.java b/pigx-register/src/main/java/com/alibaba/nacos/console/controller/UserController.java new file mode 100755 index 000000000..2daef8690 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/controller/UserController.java @@ -0,0 +1,232 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.controller; + +import com.alibaba.fastjson.JSONObject; +import com.alibaba.nacos.api.common.Constants; +import com.alibaba.nacos.config.server.auth.RoleInfo; +import com.alibaba.nacos.config.server.model.RestResult; +import com.alibaba.nacos.config.server.model.User; +import com.alibaba.nacos.console.security.nacos.NacosAuthConfig; +import com.alibaba.nacos.console.security.nacos.NacosAuthManager; +import com.alibaba.nacos.console.security.nacos.roles.NacosRoleServiceImpl; +import com.alibaba.nacos.console.security.nacos.users.NacosUser; +import com.alibaba.nacos.console.security.nacos.users.NacosUserDetailsServiceImpl; +import com.alibaba.nacos.console.utils.JwtTokenUtils; +import com.alibaba.nacos.console.utils.PasswordEncoderUtil; +import com.alibaba.nacos.core.auth.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * User related methods entry + * + * @author wfnuser + * @author nkorange + */ +@RestController("user") +@RequestMapping({"/v1/auth", "/v1/auth/users"}) +public class UserController { + + @Autowired + private JwtTokenUtils jwtTokenUtils; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private NacosUserDetailsServiceImpl userDetailsService; + + @Autowired + private NacosRoleServiceImpl roleService; + + @Autowired + private AuthConfigs authConfigs; + + @Autowired + private NacosAuthManager authManager; + + /** + * Create a new user + * + * @param username username + * @param password password + * @return ok if create succeed + * @throws IllegalArgumentException if user already exist + * @since 1.2.0 + */ + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE) + @PostMapping + public Object createUser(@RequestParam String username, @RequestParam String password) { + + User user = userDetailsService.getUserFromDatabase(username); + if (user != null) { + throw new IllegalArgumentException("user '" + username + "' already exist!"); + } + userDetailsService.createUser(username, PasswordEncoderUtil.encode(password)); + return new RestResult<>(200, "create user ok!"); + } + + /** + * Delete an existed user + * + * @param username username of user + * @return ok if deleted succeed, keep silent if user not exist + * @since 1.2.0 + */ + @DeleteMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE) + public Object deleteUser(@RequestParam String username) { + List roleInfoList = roleService.getRoles(username); + if (roleInfoList != null) { + for (RoleInfo roleInfo : roleInfoList) { + if (roleInfo.getRole().equals(NacosRoleServiceImpl.GLOBAL_ADMIN_ROLE)) { + throw new IllegalArgumentException("cannot delete admin: " + username); + } + } + } + userDetailsService.deleteUser(username); + return new RestResult<>(200, "delete user ok!"); + } + + /** + * Update an user + * + * @param username username of user + * @param newPassword new password of user + * @return ok if update succeed + * @throws IllegalArgumentException if user not exist or oldPassword is incorrect + * @since 1.2.0 + */ + @PutMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE) + public Object updateUser(@RequestParam String username, @RequestParam String newPassword) { + + User user = userDetailsService.getUserFromDatabase(username); + if (user == null) { + throw new IllegalArgumentException("user " + username + " not exist!"); + } + + userDetailsService.updateUserPassword(username, PasswordEncoderUtil.encode(newPassword)); + + return new RestResult<>(200, "update user ok!"); + } + + /** + * Get paged users + * + * @param pageNo number index of page + * @param pageSize size of page + * @return A collection of users, empty set if no user is found + * @since 1.2.0 + */ + @GetMapping + @Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.READ) + public Object getUsers(@RequestParam int pageNo, @RequestParam int pageSize) { + return userDetailsService.getUsersFromDatabase(pageNo, pageSize); + } + + /** + * Login to Nacos + *

+ * This methods uses username and password to require a new token. + * + * @param username username of user + * @param password password + * @param response http response + * @param request http request + * @return new token of the user + * @throws AccessException if user info is incorrect + */ + @PostMapping("/login") + public Object login(@RequestParam String username, @RequestParam String password, + HttpServletResponse response, HttpServletRequest request) throws AccessException { + + + if (AuthSystemTypes.NACOS.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())) { + NacosUser user = (NacosUser) authManager.login(request); + + response.addHeader(NacosAuthConfig.AUTHORIZATION_HEADER, + NacosAuthConfig.TOKEN_PREFIX + user.getToken()); + + JSONObject result = new JSONObject(); + result.put(Constants.ACCESS_TOKEN, user.getToken()); + result.put(Constants.TOKEN_TTL, authConfigs.getTokenValidityInSeconds()); + result.put(Constants.GLOBAL_ADMIN, user.isGlobalAdmin()); + return result; + } + + // 通过用户名和密码创建一个 Authentication 认证对象,实现类为 UsernamePasswordAuthenticationToken + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); + + RestResult rr = new RestResult(); + try { + //通过 AuthenticationManager(默认实现为ProviderManager)的authenticate方法验证 Authentication 对象 + Authentication authentication = authenticationManager.authenticate(authenticationToken); + //将 Authentication 绑定到 SecurityContext + SecurityContextHolder.getContext().setAuthentication(authentication); + //生成Token + String token = jwtTokenUtils.createToken(authentication); + //将Token写入到Http头部 + response.addHeader(NacosAuthConfig.AUTHORIZATION_HEADER, "Bearer " + token); + rr.setCode(200); + rr.setData("Bearer " + token); + return rr; + } catch (BadCredentialsException authentication) { + rr.setCode(401); + rr.setMessage("Login failed"); + return rr; + } + } + + @PutMapping("/password") + @Deprecated + public RestResult updatePassword(@RequestParam(value = "oldPassword") String oldPassword, + @RequestParam(value = "newPassword") String newPassword) { + + RestResult rr = new RestResult(); + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + String username = ((UserDetails) principal).getUsername(); + User user = userDetailsService.getUserFromDatabase(username); + String password = user.getPassword(); + + // TODO: throw out more fine grained exceptions + try { + if (PasswordEncoderUtil.matches(oldPassword, password)) { + userDetailsService.updateUserPassword(username, PasswordEncoderUtil.encode(newPassword)); + rr.setCode(200); + rr.setMessage("Update password success"); + } else { + rr.setCode(401); + rr.setMessage("Old password is invalid"); + } + } catch (Exception e) { + rr.setCode(500); + rr.setMessage("Update userpassword failed"); + } + return rr; + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/exception/ConsoleExceptionHandler.java b/pigx-register/src/main/java/com/alibaba/nacos/console/exception/ConsoleExceptionHandler.java new file mode 100755 index 000000000..9d9d285a3 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/exception/ConsoleExceptionHandler.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.exception; + +import com.alibaba.nacos.core.auth.AccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +/** + * Exception handler for console module + * + * @author nkorange + * @since 1.2.0 + */ +@ControllerAdvice +public class ConsoleExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(ConsoleExceptionHandler.class); + + @ExceptionHandler(AccessException.class) + private ResponseEntity handleAccessException(AccessException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(e.getErrMsg()); + } + + @ExceptionHandler(IllegalArgumentException.class) + private ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.toString()); + } + + @ExceptionHandler(Exception.class) + private ResponseEntity handleException(Exception e) { + logger.error("CONSOLE", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.toString()); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/CustomAuthenticationProvider.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/CustomAuthenticationProvider.java new file mode 100755 index 000000000..c54db33f7 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/CustomAuthenticationProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos; + +import com.alibaba.nacos.console.security.nacos.users.NacosUserDetailsServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +/** + * auth provider + * + * @author wfnuser + */ +@Component +public class CustomAuthenticationProvider implements AuthenticationProvider { + + @Autowired + private NacosUserDetailsServiceImpl userDetailsService; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + String username = (String) authentication.getPrincipal(); + String password = (String) authentication.getCredentials(); + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + if (!password.equals(userDetails.getPassword())) { + return new UsernamePasswordAuthenticationToken(username, null, null); + } + return null; + } + + @Override + public boolean supports(Class aClass) { + return aClass.equals(UsernamePasswordAuthenticationToken.class); + } + +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtAuthenticationEntryPoint.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtAuthenticationEntryPoint.java new file mode 100755 index 000000000..dad99758c --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtAuthenticationEntryPoint.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * jwt auth fail point + * + * @author wfnuser + */ +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException e) throws IOException, ServletException { + logger.error("Responding with unauthorized error. Message:{}, url:{}", e.getMessage(), request.getRequestURI()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtTokenManager.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtTokenManager.java new file mode 100755 index 000000000..0ef8fce1f --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/JwtTokenManager.java @@ -0,0 +1,104 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos; + +import com.alibaba.nacos.core.auth.AuthConfigs; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Date; +import java.util.List; + +/** + * JWT token manager + * + * @author wfnuser + * @author nkorange + */ +@Component +public class JwtTokenManager { + + private static final String AUTHORITIES_KEY = "auth"; + + @Autowired + private AuthConfigs authConfigs; + + /** + * Create token + * + * @param authentication auth info + * @return token + */ + public String createToken(Authentication authentication) { + return createToken(authentication.getName()); + } + + public String createToken(String userName) { + + long now = (new Date()).getTime(); + + Date validity; + validity = new Date(now + authConfigs.getTokenValidityInSeconds() * 1000L); + + Claims claims = Jwts.claims().setSubject(userName); + + return Jwts.builder() + .setClaims(claims) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, authConfigs.getSecretKey()) + .compact(); + } + + /** + * Get auth Info + * + * @param token token + * @return auth info + */ + public Authentication getAuthentication(String token) { + /** + * parse the payload of token + */ + Claims claims = Jwts.parser() + .setSigningKey(authConfigs.getSecretKey()) + .parseClaimsJws(token) + .getBody(); + + List authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get(AUTHORITIES_KEY)); + + User principal = new User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + /** + * validate token + * + * @param token token + * @return whether valid + */ + public void validateToken(String token) { + Jwts.parser().setSigningKey(authConfigs.getSecretKey()).parseClaimsJws(token); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthConfig.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthConfig.java new file mode 100755 index 000000000..78fe636b5 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthConfig.java @@ -0,0 +1,146 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos; + + +import com.alibaba.nacos.console.filter.JwtAuthenticationTokenFilter; +import com.alibaba.nacos.console.security.nacos.users.NacosUserDetailsServiceImpl; +import com.alibaba.nacos.core.auth.AuthConfigs; +import com.alibaba.nacos.core.auth.AuthSystemTypes; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsUtils; + +/** + * Spring security config + * + * @author Nacos + */ +@Configuration +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class NacosAuthConfig extends WebSecurityConfigurerAdapter { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + public static final String SECURITY_IGNORE_URLS_SPILT_CHAR = ","; + + public static final String LOGIN_ENTRY_POINT = "/v1/auth/login"; + + public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/v1/auth/**"; + + public static final String TOKEN_PREFIX = "Bearer "; + + public static final String CONSOLE_RESOURCE_NAME_PREFIX = "console/"; + + @Autowired + private Environment env; + + @Autowired + private JwtTokenManager tokenProvider; + + @Autowired + private AuthConfigs authConfigs; + + @Autowired + private NacosUserDetailsServiceImpl userDetailsService; + + @Value("${nacos.security.ignore.urls}") + private String ignoreURLs; + + @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + public void configure(WebSecurity web) { + + if (AuthSystemTypes.NACOS.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())) { + ignoreURLs = "/**"; + } +// + if (StringUtils.isBlank(authConfigs.getNacosAuthSystemType())) { + ignoreURLs = env.getProperty("nacos.security.ignore.urls", "/**"); + } + + if (StringUtils.isNotBlank(ignoreURLs)) { + for (String ignoreURL : ignoreURLs.trim().split(SECURITY_IGNORE_URLS_SPILT_CHAR)) { + web.ignoring().antMatchers(ignoreURL.trim()); + } + } + + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + + if (StringUtils.isBlank(authConfigs.getNacosAuthSystemType())) { + http + + .csrf().disable() + .cors() // We don't need CSRF for JWT based authentication + + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + + .and() + .authorizeRequests() + .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() + .antMatchers(LOGIN_ENTRY_POINT).permitAll() + + .and() + .authorizeRequests() + .antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated() + + .and() + .exceptionHandling() + .authenticationEntryPoint(new JwtAuthenticationEntryPoint()); + + // disable cache + http.headers().cacheControl(); + + http.addFilterBefore(new JwtAuthenticationTokenFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class); + } + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthManager.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthManager.java new file mode 100755 index 000000000..bd6d12293 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/NacosAuthManager.java @@ -0,0 +1,136 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos; + +import com.alibaba.nacos.api.common.Constants; +import com.alibaba.nacos.config.server.auth.RoleInfo; +import com.alibaba.nacos.console.security.nacos.roles.NacosRoleServiceImpl; +import com.alibaba.nacos.console.security.nacos.users.NacosUser; +import com.alibaba.nacos.core.auth.AccessException; +import com.alibaba.nacos.core.auth.AuthManager; +import com.alibaba.nacos.core.auth.Permission; +import com.alibaba.nacos.core.auth.User; +import com.alibaba.nacos.core.utils.Loggers; +import io.jsonwebtoken.ExpiredJwtException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +/** + * Builtin access control entry of Nacos + * + * @author nkorange + * @since 1.2.0 + */ +@Component +public class NacosAuthManager implements AuthManager { + + private static final String TOKEN_PREFIX = "Bearer "; + + @Autowired + private JwtTokenManager tokenManager; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private NacosRoleServiceImpl roleService; + + @Override + public User login(Object request) throws AccessException { + HttpServletRequest req = (HttpServletRequest) request; + String token = resolveToken(req); + if (StringUtils.isBlank(token)) { + throw new AccessException("user not found!"); + } + + try { + tokenManager.validateToken(token); + } catch (ExpiredJwtException e) { + throw new AccessException("token expired!"); + } catch (Exception e) { + throw new AccessException("token invalid!"); + } + + Authentication authentication = tokenManager.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + + String username = authentication.getName(); + NacosUser user = new NacosUser(); + user.setUserName(username); + user.setToken(token); + List roleInfoList = roleService.getRoles(username); + if (roleInfoList != null) { + for (RoleInfo roleInfo : roleInfoList) { + if (roleInfo.getRole().equals(NacosRoleServiceImpl.GLOBAL_ADMIN_ROLE)) { + user.setGlobalAdmin(true); + break; + } + } + } + + return user; + } + + @Override + public void auth(Permission permission, User user) throws AccessException { + if (Loggers.AUTH.isDebugEnabled()) { + Loggers.AUTH.debug("auth permission: {}, user: {}", permission, user); + } + + if (!roleService.hasPermission(user.getUserName(), permission)) { + throw new AccessException("authorization failed!"); + } + } + + /** + * Get token from header + */ + private String resolveToken(HttpServletRequest request) throws AccessException { + String bearerToken = request.getHeader(NacosAuthConfig.AUTHORIZATION_HEADER); + if (StringUtils.isNotBlank(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) { + return bearerToken.substring(7); + } + bearerToken = request.getParameter(Constants.ACCESS_TOKEN); + if (StringUtils.isBlank(bearerToken)) { + String userName = request.getParameter("username"); + String password = request.getParameter("password"); + bearerToken = resolveTokenFromUser(userName, password); + } + + return bearerToken; + } + + private String resolveTokenFromUser(String userName, String rawPassword) throws AccessException { + + try { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, rawPassword); + authenticationManager.authenticate(authenticationToken); + } catch (AuthenticationException e) { + throw new AccessException("unknown user!"); + } + + return tokenManager.createToken(userName); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/roles/NacosRoleServiceImpl.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/roles/NacosRoleServiceImpl.java new file mode 100755 index 000000000..0799c1f31 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/roles/NacosRoleServiceImpl.java @@ -0,0 +1,219 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos.roles; + + +import com.alibaba.nacos.config.server.auth.PermissionInfo; +import com.alibaba.nacos.config.server.auth.PermissionPersistService; +import com.alibaba.nacos.config.server.auth.RoleInfo; +import com.alibaba.nacos.config.server.auth.RolePersistService; +import com.alibaba.nacos.config.server.model.Page; +import com.alibaba.nacos.console.security.nacos.NacosAuthConfig; +import com.alibaba.nacos.console.security.nacos.users.NacosUserDetailsServiceImpl; +import com.alibaba.nacos.core.auth.AuthConfigs; +import com.alibaba.nacos.core.auth.Permission; +import com.alibaba.nacos.core.utils.Loggers; +import io.jsonwebtoken.lang.Collections; +import org.apache.commons.lang3.StringUtils; +import org.apache.mina.util.ConcurrentHashSet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * Nacos builtin role service. + * + * @author nkorange + * @since 1.2.0 + */ +@Service +public class NacosRoleServiceImpl { + + public static final String GLOBAL_ADMIN_ROLE = "ROLE_ADMIN"; + + @Autowired + private AuthConfigs authConfigs; + + @Autowired + private RolePersistService rolePersistService; + + @Autowired + private NacosUserDetailsServiceImpl userDetailsService; + + @Autowired + private PermissionPersistService permissionPersistService; + + private Set roleSet = new ConcurrentHashSet<>(); + + private Map> roleInfoMap = new ConcurrentHashMap<>(); + + private Map> permissionInfoMap = new ConcurrentHashMap<>(); + + @Scheduled(initialDelay = 5000, fixedDelay = 15000) + private void reload() { + try { + Page roleInfoPage = rolePersistService.getRolesByUserName(StringUtils.EMPTY, 1, Integer.MAX_VALUE); + if (roleInfoPage == null) { + return; + } + Set tmpRoleSet = new HashSet<>(16); + Map> tmpRoleInfoMap = new ConcurrentHashMap<>(16); + for (RoleInfo roleInfo : roleInfoPage.getPageItems()) { + if (!tmpRoleInfoMap.containsKey(roleInfo.getUsername())) { + tmpRoleInfoMap.put(roleInfo.getUsername(), new ArrayList<>()); + } + tmpRoleInfoMap.get(roleInfo.getUsername()).add(roleInfo); + tmpRoleSet.add(roleInfo.getRole()); + } + + Map> tmpPermissionInfoMap = new ConcurrentHashMap<>(16); + for (String role : tmpRoleSet) { + Page permissionInfoPage = permissionPersistService.getPermissions(role, 1, Integer.MAX_VALUE); + tmpPermissionInfoMap.put(role, permissionInfoPage.getPageItems()); + } + + roleSet = tmpRoleSet; + roleInfoMap = tmpRoleInfoMap; + permissionInfoMap = tmpPermissionInfoMap; + } catch (Exception e) { + Loggers.AUTH.warn("[LOAD-ROLES] load failed", e); + } + } + + /** + * Determine if the user has permission of the resource. + *

+ * Note if the user has many roles, this method returns true if any one role of the user has the + * desired permission. + * + * @param username user info + * @param permission permission to auth + * @return true if granted, false otherwise + */ + public boolean hasPermission(String username, Permission permission) { + + List roleInfoList = getRoles(username); + if (Collections.isEmpty(roleInfoList)) { + return false; + } + + // Global admin pass: + for (RoleInfo roleInfo : roleInfoList) { + if (GLOBAL_ADMIN_ROLE.equals(roleInfo.getRole())) { + return true; + } + } + + // Old global admin can pass resource 'console/': + if (permission.getResource().startsWith(NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX)) { + return false; + } + + // For other roles, use a pattern match to decide if pass or not. + for (RoleInfo roleInfo : roleInfoList) { + List permissionInfoList = getPermissions(roleInfo.getRole()); + if (Collections.isEmpty(permissionInfoList)) { + continue; + } + for (PermissionInfo permissionInfo : permissionInfoList) { + String permissionResource = permissionInfo.getResource().replaceAll("\\*", ".*"); + String permissionAction = permissionInfo.getAction(); + if (permissionAction.contains(permission.getAction()) && + Pattern.matches(permissionResource, permission.getResource())) { + return true; + } + } + } + return false; + } + + public List getRoles(String username) { + List roleInfoList = roleInfoMap.get(username); + if (!authConfigs.isCachingEnabled()) { + Page roleInfoPage = getRolesFromDatabase(username, 1, Integer.MAX_VALUE); + if (roleInfoPage != null) { + roleInfoList = roleInfoPage.getPageItems(); + } + } + return roleInfoList; + } + + public Page getRolesFromDatabase(String userName, int pageNo, int pageSize) { + Page roles = rolePersistService.getRolesByUserName(userName, pageNo, pageSize); + if (roles == null) { + return new Page<>(); + } + return roles; + } + + public List getPermissions(String role) { + List permissionInfoList = permissionInfoMap.get(role); + if (!authConfigs.isCachingEnabled()) { + Page permissionInfoPage = getPermissionsFromDatabase(role, 1, Integer.MAX_VALUE); + if (permissionInfoPage != null) { + permissionInfoList = permissionInfoPage.getPageItems(); + } + } + return permissionInfoList; + } + + public Page getPermissionsByRoleFromDatabase(String role, int pageNo, int pageSize) { + return permissionPersistService.getPermissions(role, pageNo, pageSize); + } + + public void addRole(String role, String username) { + if (userDetailsService.getUser(username) == null) { + throw new IllegalArgumentException("user '" + username + "' not found!"); + } + if (GLOBAL_ADMIN_ROLE.equals(role)) { + throw new IllegalArgumentException("role '" + GLOBAL_ADMIN_ROLE + "' is not permitted to create!"); + } + rolePersistService.addRole(role, username); + roleSet.add(role); + } + + public void deleteRole(String role, String userName) { + rolePersistService.deleteRole(role, userName); + } + + public void deleteRole(String role) { + rolePersistService.deleteRole(role); + roleSet.remove(role); + } + + public Page getPermissionsFromDatabase(String role, int pageNo, int pageSize) { + Page pageInfo = permissionPersistService.getPermissions(role, pageNo, pageSize); + if (pageInfo == null) { + return new Page<>(); + } + return pageInfo; + } + + public void addPermission(String role, String resource, String action) { + if (!roleSet.contains(role)) { + throw new IllegalArgumentException("role " + role + " not found!"); + } + permissionPersistService.addPermission(role, resource, action); + } + + public void deletePermission(String role, String resource, String action) { + permissionPersistService.deletePermission(role, resource, action); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUser.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUser.java new file mode 100755 index 000000000..f2f519c58 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUser.java @@ -0,0 +1,52 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos.users; + +import com.alibaba.fastjson.JSON; +import com.alibaba.nacos.core.auth.User; + +/** + * @author nkorange + * @since 1.2.0 + */ +public class NacosUser extends User { + + private String token; + + private boolean globalAdmin = false; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public boolean isGlobalAdmin() { + return globalAdmin; + } + + public void setGlobalAdmin(boolean globalAdmin) { + this.globalAdmin = globalAdmin; + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetails.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetails.java new file mode 100755 index 000000000..3390a2ce2 --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetails.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos.users; + +import com.alibaba.nacos.config.server.model.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +/** + * custem user + * + * @author wfnuser + */ +public class NacosUserDetails implements UserDetails { + + private User user; + + public NacosUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + // TODO: get authorities + return AuthorityUtils.commaSeparatedStringToAuthorityList(""); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetailsServiceImpl.java b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetailsServiceImpl.java new file mode 100755 index 000000000..7c9b66ceb --- /dev/null +++ b/pigx-register/src/main/java/com/alibaba/nacos/console/security/nacos/users/NacosUserDetailsServiceImpl.java @@ -0,0 +1,110 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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.alibaba.nacos.console.security.nacos.users; + + +import com.alibaba.nacos.config.server.auth.UserPersistService; +import com.alibaba.nacos.config.server.model.Page; +import com.alibaba.nacos.config.server.model.User; +import com.alibaba.nacos.core.auth.AuthConfigs; +import com.alibaba.nacos.core.utils.Loggers; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Custem user service + * + * @author wfnuser + * @author nkorange + */ +@Service +public class NacosUserDetailsServiceImpl implements UserDetailsService { + + private Map userMap = new ConcurrentHashMap<>(); + + @Autowired + private UserPersistService userPersistService; + + @Autowired + private AuthConfigs authConfigs; + + @Scheduled(initialDelay = 5000, fixedDelay = 15000) + private void reload() { + try { + Page users = getUsersFromDatabase(1, Integer.MAX_VALUE); + if (users == null) { + return; + } + + Map map = new ConcurrentHashMap<>(16); + for (User user : users.getPageItems()) { + map.put(user.getUsername(), user); + } + userMap = map; + } catch (Exception e) { + Loggers.AUTH.warn("[LOAD-USERS] load failed", e); + } + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + User user = userMap.get(username); + if (!authConfigs.isCachingEnabled()) { + user = userPersistService.findUserByUsername(username); + } + + if (user == null) { + throw new UsernameNotFoundException(username); + } + return new NacosUserDetails(user); + } + + public void updateUserPassword(String username, String password) { + userPersistService.updateUserPassword(username, password); + } + + public Page getUsersFromDatabase(int pageNo, int pageSize) { + return userPersistService.getUsers(pageNo, pageSize); + } + + public User getUser(String username) { + User user = userMap.get(username); + if (!authConfigs.isCachingEnabled()) { + user = getUserFromDatabase(username); + } + return user; + } + + public User getUserFromDatabase(String username) { + return userPersistService.findUserByUsername(username); + } + + public void createUser(String username, String password) { + userPersistService.createUser(username, password); + } + + public void deleteUser(String username) { + userPersistService.deleteUser(username); + } +} diff --git a/pigx-register/src/main/resources/static/console-fe/src/layouts/menu.js b/pigx-register/src/main/resources/static/console-fe/src/layouts/menu.js new file mode 100755 index 000000000..0143f0003 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/layouts/menu.js @@ -0,0 +1,75 @@ +import { isJsonString } from '../utils/nacosutil'; + +const configurationMenu = { + key: 'configurationManagementVirtual', + children: [ + { + key: 'configurationManagement', + url: '/configurationManagement', + }, + { + key: 'historyRollback', + url: '/historyRollback', + }, + { + key: 'listeningToQuery', + url: '/listeningToQuery', + }, + ], +}; +/** + * 权限控制相关 + */ +const authorityControlMenu = { + key: 'authorityControl', + children: [ + { + key: 'userList', + url: '/userManagement', + }, + { + key: 'roleManagement', + url: '/rolesManagement', + }, + { + key: 'privilegeManagement', + url: '/permissionsManagement', + }, + ], +}; + +export default function(model) { + const { token = '{}' } = localStorage; + const { globalAdmin } = isJsonString(token) ? JSON.parse(token) || {} : {}; + + return [ + model === 'naming' ? undefined : configurationMenu, + { + key: 'serviceManagementVirtual', + children: [ + { + key: 'serviceManagement', + url: '/serviceManagement', + }, + { + key: 'subscriberList', + url: '/subscriberList', + }, + ], + }, + { + key: 'clusterManagementVirtual', + children: [ + { + key: 'clusterManagement', + url: '/clusterManagement', + }, + ], + }, + globalAdmin ? authorityControlMenu : undefined, + { + key: 'namespace', + url: '/namespace', + }, + ].filter(item => item); +} diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/NewPermissions.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/NewPermissions.js new file mode 100755 index 000000000..a54d0f6f4 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/NewPermissions.js @@ -0,0 +1,121 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, Form, Input, Select, Dialog, ConfigProvider } from '@alifd/next'; +import { connect } from 'react-redux'; +import { getNamespaces } from '../../../reducers/namespace'; + +const FormItem = Form.Item; +const { Option } = Select; + +const formItemLayout = { + labelCol: { fixedSpan: 4 }, + wrapperCol: { span: 19 }, +}; + +@connect(state => ({ namespaces: state.namespace.namespaces }), { getNamespaces }) +@ConfigProvider.config +class NewPermissions extends React.Component { + static displayName = 'NewPermissions'; + + field = new Field(this); + + static propTypes = { + locale: PropTypes.object, + visible: PropTypes.bool, + getNamespaces: PropTypes.func, + onOk: PropTypes.func, + onCancel: PropTypes.func, + namespaces: PropTypes.array, + }; + + componentDidMount() { + this.props.getNamespaces(); + } + + check() { + const { locale } = this.props; + const errors = { + role: locale.roleError, + resource: locale.resourceError, + action: locale.actionError, + }; + const vals = Object.keys(errors).map(key => { + const val = this.field.getValue(key); + if (!val) { + this.field.setError(key, errors[key]); + } + return val; + }); + if (vals.filter(v => v).length === 3) { + return vals; + } + return null; + } + + render() { + const { getError } = this.field; + const { visible, onOk, onCancel, locale, namespaces } = this.props; + return ( + <> +

{ + const vals = this.check(); + if (vals) { + onOk(vals).then(() => onCancel()); + } + }} + onClose={onCancel} + onCancel={onCancel} + afterClose={() => this.field.reset()} + > +
+ + + + + + + + + +
+
+ + ); + } +} + +export default NewPermissions; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js new file mode 100755 index 000000000..614c42adf --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js @@ -0,0 +1,162 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Dialog, Pagination, Table, ConfigProvider } from '@alifd/next'; +import { connect } from 'react-redux'; +import { getPermissions, createPermission, deletePermission } from '../../../reducers/authority'; +import { getNamespaces } from '../../../reducers/namespace'; +import RegionGroup from '../../../components/RegionGroup'; +import NewPermissions from './NewPermissions'; + +import './PermissionsManagement.scss'; + +@connect( + state => ({ + permissions: state.authority.permissions, + namespaces: state.namespace.namespaces, + }), + { getPermissions, getNamespaces } +) +@ConfigProvider.config +class PermissionsManagement extends React.Component { + static displayName = 'PermissionsManagement'; + + static propTypes = { + locale: PropTypes.object, + permissions: PropTypes.object, + namespaces: PropTypes.object, + getPermissions: PropTypes.func, + getNamespaces: PropTypes.func, + }; + + constructor(props) { + super(props); + this.state = { + loading: true, + pageNo: 1, + pageSize: 9, + createPermission: false, + }; + } + + componentDidMount() { + this.getPermissions(); + this.props.getNamespaces(); + } + + getPermissions() { + const { pageNo, pageSize } = this.state; + this.props + .getPermissions({ pageNo, pageSize }) + .then(() => { + if (this.state.loading) { + this.setState({ loading: false }); + } + }) + .catch(() => this.setState({ loading: false })); + } + + colseCreatePermission() { + this.setState({ createPermissionVisible: false }); + } + + getActionText(action) { + const { locale } = this.props; + return { + r: `${locale.readOnly} (r)`, + w: `${locale.writeOnly} (w)`, + rw: `${locale.readWrite} (rw)`, + }[action]; + } + + render() { + const { permissions, namespaces = [], locale } = this.props; + const { loading, pageSize, pageNo, createPermissionVisible } = this.state; + return ( + <> + +
+ +
+ + + { + const [item = {}] = namespaces.filter(({ namespace }) => { + const [itemNamespace] = value.split(':'); + return itemNamespace === namespace; + }); + const { namespaceShowName = '', namespace = '' } = item; + return namespaceShowName + (namespace ? ` (${namespace})` : ''); + }} + /> + this.getActionText(action)} + /> + ( + <> + + + )} + /> +
+ {permissions.totalCount > pageSize && ( + this.setState({ pageNo }, () => this.getPermissions())} + /> + )} + + createPermission(permission).then(res => { + this.setState({ pageNo: 1 }, () => this.getPermissions()); + return res; + }) + } + onCancel={() => this.colseCreatePermission()} + /> + + ); + } +} + +export default PermissionsManagement; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.scss b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.scss new file mode 100755 index 000000000..03175a7be --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.scss @@ -0,0 +1,12 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ \ No newline at end of file diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/index.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/index.js new file mode 100755 index 000000000..cfd9f7839 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/PermissionsManagement/index.js @@ -0,0 +1,16 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import PermissionsManagement from './PermissionsManagement'; + +export default PermissionsManagement; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/README.md b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/README.md new file mode 100755 index 000000000..ba2cd2c97 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/README.md @@ -0,0 +1,7 @@ +# 权限控制 + +> AuthorityControl + +1. UserManagement => 用户管理 +2. RolesManagement => 角色管理 +3. PermissionsManagement => 权限管理 \ No newline at end of file diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/NewRole.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/NewRole.js new file mode 100755 index 000000000..4628377af --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/NewRole.js @@ -0,0 +1,90 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, Form, Input, Dialog, ConfigProvider } from '@alifd/next'; + +const FormItem = Form.Item; + +const formItemLayout = { + labelCol: { fixedSpan: 4 }, + wrapperCol: { span: 19 }, +}; + +@ConfigProvider.config +class NewRole extends React.Component { + static displayName = 'NewRole'; + + field = new Field(this); + + static propTypes = { + locale: PropTypes.object, + visible: PropTypes.bool, + onOk: PropTypes.func, + onCancel: PropTypes.func, + }; + + check() { + const { locale } = this.props; + const errors = { + role: locale.roleError, + username: locale.usernameError, + }; + const vals = Object.keys(errors).map(key => { + const val = this.field.getValue(key); + if (!val) { + this.field.setError(key, errors[key]); + } + return val; + }); + if (vals.filter(v => v).length === 2) { + return vals; + } + return null; + } + + render() { + const { locale } = this.props; + const { getError } = this.field; + const { visible, onOk, onCancel } = this.props; + return ( + <> + { + const vals = this.check(); + if (vals) { + onOk(vals).then(() => onCancel()); + } + }} + onClose={onCancel} + onCancel={onCancel} + afterClose={() => this.field.reset()} + > +
+ + + + + + +
+
+ + ); + } +} + +export default NewRole; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.js new file mode 100755 index 000000000..5fca1f7c5 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.js @@ -0,0 +1,130 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Dialog, Pagination, Table, ConfigProvider } from '@alifd/next'; +import { connect } from 'react-redux'; +import { getRoles, createRole, deleteRole } from '../../../reducers/authority'; +import RegionGroup from '../../../components/RegionGroup'; +import NewRole from './NewRole'; + +import './RolesManagement.scss'; + +@connect(state => ({ roles: state.authority.roles }), { getRoles }) +@ConfigProvider.config +class RolesManagement extends React.Component { + static displayName = 'RolesManagement'; + + static propTypes = { + locale: PropTypes.object, + roles: PropTypes.object, + getRoles: PropTypes.func, + }; + + constructor(props) { + super(props); + this.state = { + loading: true, + pageNo: 1, + pageSize: 9, + }; + } + + componentDidMount() { + this.getRoles(); + } + + getRoles() { + const { pageNo, pageSize } = this.state; + this.props + .getRoles({ pageNo, pageSize }) + .then(() => { + if (this.state.loading) { + this.setState({ loading: false }); + } + }) + .catch(() => this.setState({ loading: false })); + } + + colseCreateRole() { + this.setState({ createRoleVisible: false }); + } + + render() { + const { roles, locale } = this.props; + const { loading, pageSize, pageNo, createRoleVisible, passwordResetUser } = this.state; + return ( + <> + +
+ +
+ + + + { + if (value === 'ROLE_ADMIN') { + return null; + } + return ( + + ); + }} + /> +
+ {roles.totalCount > pageSize && ( + this.setState({ pageNo }, () => this.getRoles())} + /> + )} + + createRole(role).then(res => { + this.getRoles(); + return res; + }) + } + onCancel={() => this.colseCreateRole()} + /> + + ); + } +} + +export default RolesManagement; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.scss b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.scss new file mode 100755 index 000000000..03175a7be --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/RolesManagement.scss @@ -0,0 +1,12 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ \ No newline at end of file diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/index.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/index.js new file mode 100755 index 000000000..6eaec98b8 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/RolesManagement/index.js @@ -0,0 +1,16 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import RolesManagement from './RolesManagement'; + +export default RolesManagement; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/NewUser.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/NewUser.js new file mode 100755 index 000000000..48e09a8e3 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/NewUser.js @@ -0,0 +1,104 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, Form, Input, Dialog, ConfigProvider } from '@alifd/next'; +import './UserManagement.scss'; + +const FormItem = Form.Item; + +const formItemLayout = { + labelCol: { fixedSpan: 4 }, + wrapperCol: { span: 19 }, +}; + +@ConfigProvider.config +class NewUser extends React.Component { + static displayName = 'NewUser'; + + field = new Field(this); + + static propTypes = { + locale: PropTypes.object, + visible: PropTypes.bool, + onOk: PropTypes.func, + onCancel: PropTypes.func, + }; + + check() { + const { locale } = this.props; + const errors = { + username: locale.usernameError, + password: locale.passwordError, + rePassword: locale.rePasswordError, + }; + const vals = Object.keys(errors).map(key => { + const val = this.field.getValue(key); + if (!val) { + this.field.setError(key, errors[key]); + } + return val; + }); + if (vals.filter(v => v).length !== 3) { + return null; + } + const [password, rePassword] = ['password', 'rePassword'].map(k => this.field.getValue(k)); + if (password !== rePassword) { + this.field.setError('rePassword', locale.rePasswordError2); + return null; + } + return vals; + } + + render() { + const { locale } = this.props; + const { getError } = this.field; + const { visible, onOk, onCancel } = this.props; + return ( + <> + { + const vals = this.check(); + if (vals) { + onOk(vals).then(() => onCancel()); + } + }} + onClose={onCancel} + onCancel={onCancel} + afterClose={() => this.field.reset()} + > +
+ + + + + + + + + +
+
+ + ); + } +} + +export default NewUser; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/PasswordReset.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/PasswordReset.js new file mode 100755 index 000000000..b381a298a --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/PasswordReset.js @@ -0,0 +1,104 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Field, Form, Input, Dialog, ConfigProvider } from '@alifd/next'; +import './UserManagement.scss'; + +const FormItem = Form.Item; + +const formItemLayout = { + labelCol: { fixedSpan: 4 }, + wrapperCol: { span: 19 }, +}; + +@ConfigProvider.config +class PasswordReset extends React.Component { + static displayName = 'PasswordReset'; + + field = new Field(this); + + static propTypes = { + locale: PropTypes.object, + visible: PropTypes.bool, + username: PropTypes.string, + onCancel: PropTypes.func, + onOk: PropTypes.func, + }; + + check() { + const { locale } = this.props; + const errors = { + password: locale.passwordError, + rePassword: locale.rePasswordError, + }; + const vals = Object.keys(errors).map(key => { + const val = this.field.getValue(key); + if (!val) { + this.field.setError(key, errors[key]); + } + return val; + }); + if (vals.filter(v => v).length !== 2) { + return null; + } + const [password, rePassword] = ['password', 'rePassword'].map(k => this.field.getValue(k)); + if (password !== rePassword) { + this.field.setError('rePassword', locale.rePasswordError2); + return null; + } + return [this.props.username, ...vals]; + } + + render() { + const { locale } = this.props; + const { getError } = this.field; + const { username, onOk, onCancel } = this.props; + return ( + <> + { + const vals = this.check(); + if (vals) { + onOk(vals).then(() => onCancel()); + } + }} + onClose={onCancel} + onCancel={onCancel} + afterClose={() => this.field.reset()} + > +
+ +

{username}

+
+ + + + + + +
+
+ + ); + } +} + +export default PasswordReset; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.js new file mode 100755 index 000000000..22713b9d1 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.js @@ -0,0 +1,150 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Dialog, Pagination, Table, ConfigProvider } from '@alifd/next'; +import { connect } from 'react-redux'; +import { getUsers, createUser, deleteUser, passwordReset } from '../../../reducers/authority'; +import RegionGroup from '../../../components/RegionGroup'; +import NewUser from './NewUser'; +import PasswordReset from './PasswordReset'; + +import './UserManagement.scss'; + +@connect(state => ({ users: state.authority.users }), { getUsers }) +@ConfigProvider.config +class UserManagement extends React.Component { + static displayName = 'UserManagement'; + + static propTypes = { + locale: PropTypes.object, + users: PropTypes.object, + getUsers: PropTypes.func, + createUser: PropTypes.func, + }; + + constructor(props) { + super(props); + this.state = { + loading: true, + pageNo: 1, + pageSize: 9, + }; + } + + componentDidMount() { + this.getUsers(); + } + + getUsers() { + const { pageNo, pageSize } = this.state; + this.props + .getUsers({ pageNo, pageSize }) + .then(() => { + if (this.state.loading) { + this.setState({ loading: false }); + } + }) + .catch(() => this.setState({ loading: false })); + } + + colseCreateUser() { + this.setState({ createUserVisible: false }); + } + + render() { + const { users, locale } = this.props; + const { loading, pageSize, pageNo, createUserVisible, passwordResetUser } = this.state; + return ( + <> + +
+ +
+ + + value.replace(/\S/g, '*')} + /> + ( + <> + +     + + + )} + /> +
+ {users.totalCount > pageSize && ( + this.setState({ pageNo }, () => this.getUsers())} + /> + )} + + createUser(user).then(res => { + this.setState({ pageNo: 1 }, () => this.getUsers()); + return res; + }) + } + onCancel={() => this.colseCreateUser()} + /> + + passwordReset(user).then(res => { + this.getUsers(); + return res; + }) + } + onCancel={() => this.setState({ passwordResetUser: undefined })} + /> + + ); + } +} + +export default UserManagement; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.scss b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.scss new file mode 100755 index 000000000..20cf13723 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/UserManagement.scss @@ -0,0 +1,18 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ +@import '../authority.scss'; + +.users-pagination { + float: right; + margin-top: 20px; +} diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/index.js b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/index.js new file mode 100755 index 000000000..4df885840 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/UserManagement/index.js @@ -0,0 +1,16 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import UserManagement from './UserManagement'; + +export default UserManagement; diff --git a/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/authority.scss b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/authority.scss new file mode 100755 index 000000000..a1112a3ef --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/pages/AuthorityControl/authority.scss @@ -0,0 +1,17 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +.filter-panel { + text-align: right; + padding: 10px 0; +} diff --git a/pigx-register/src/main/resources/static/console-fe/src/reducers/authority.js b/pigx-register/src/main/resources/static/console-fe/src/reducers/authority.js new file mode 100755 index 000000000..0ee7ef288 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/reducers/authority.js @@ -0,0 +1,143 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import { Message } from '@alifd/next'; +import request from '../utils/request'; +import { UPDATE_USER, SIGN_IN, USER_LIST, ROLE_LIST, PERMISSIONS_LIST } from '../constants'; + +const initialState = { + users: { + totalCount: 0, + pageNumber: 1, + pagesAvailable: 1, + pageItems: [], + }, + roles: { + totalCount: 0, + pageNumber: 1, + pagesAvailable: 1, + pageItems: [], + }, + permissions: { + totalCount: 0, + pageNumber: 1, + pagesAvailable: 1, + pageItems: [], + }, +}; + +const successMsg = res => { + if (res.code === 200) { + Message.success(res.message); + } + return res; +}; + +/** + * 用户列表 + * @param {*} params + */ +const getUsers = params => dispatch => + request.get('v1/auth/users', { params }).then(data => dispatch({ type: USER_LIST, data })); + +/** + * 创建用户 + * @param {*} param0 + */ +const createUser = ([username, password]) => + request.post('v1/auth/users', { username, password }).then(res => successMsg(res)); + +/** + * 删除用户 + * @param {*} username + */ +const deleteUser = username => + request.delete('v1/auth/users', { params: { username } }).then(res => successMsg(res)); + +/** + * 重置密码 + * @param {*} param0 + */ +const passwordReset = ([username, newPassword]) => + request.put('v1/auth/users', { username, newPassword }).then(res => successMsg(res)); + +/** + * 角色列表 + * @param {*} params + */ + +const getRoles = params => dispatch => + request.get('v1/auth/roles', { params }).then(data => dispatch({ type: ROLE_LIST, data })); + +/** + * 创建角色 + * @param {*} param0 + */ +const createRole = ([role, username]) => + request.post('v1/auth/roles', { role, username }).then(res => successMsg(res)); + +/** + * 删除角色 + * @param {*} param0 + */ +const deleteRole = role => + request.delete('v1/auth/roles', { params: role }).then(res => successMsg(res)); + +/** + * 权限列表 + * @param {*} params + */ +const getPermissions = params => dispatch => + request + .get('v1/auth/permissions', { params }) + .then(data => dispatch({ type: PERMISSIONS_LIST, data })); + +/** + * 给角色添加权限 + * @param {*} param0 + */ +const createPermission = ([role, resource, action]) => + request.post('v1/auth/permissions', { role, resource, action }).then(res => successMsg(res)); + +/** + * 删除权限 + * @param {*} param0 + */ +const deletePermission = permission => + request.delete('v1/auth/permissions', { params: permission }).then(res => successMsg(res)); + +export default (state = initialState, action) => { + switch (action.type) { + case USER_LIST: + return { ...state, users: { ...action.data } }; + case ROLE_LIST: + return { ...state, roles: { ...action.data } }; + case PERMISSIONS_LIST: + return { ...state, permissions: { ...action.data } }; + default: + return state; + } +}; + +export { + getUsers, + createUser, + deleteUser, + passwordReset, + getRoles, + createRole, + deleteRole, + getPermissions, + createPermission, + deletePermission, +}; diff --git a/pigx-register/src/main/resources/static/console-fe/src/reducers/configuration.js b/pigx-register/src/main/resources/static/console-fe/src/reducers/configuration.js new file mode 100755 index 000000000..8e5eacf05 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/reducers/configuration.js @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import request from '../utils/request'; +import { GET_CONFIGURATION } from '../constants'; + +const initialState = { + configurations: [], +}; + +const getConfigs = params => dispatch => + request + .get('v1/cs/configs', { params }) + .then(data => dispatch({ type: GET_CONFIGURATION, data })); + +export default (state = initialState, action) => { + switch (action.type) { + case GET_CONFIGURATION: + return { ...state, configurations: action.data }; + default: + return state; + } +}; + +export { getConfigs }; diff --git a/pigx-register/src/main/resources/static/console-fe/src/reducers/namespace.js b/pigx-register/src/main/resources/static/console-fe/src/reducers/namespace.js new file mode 100755 index 000000000..895bf0f22 --- /dev/null +++ b/pigx-register/src/main/resources/static/console-fe/src/reducers/namespace.js @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * 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. + */ + +import request from '../utils/request'; +import { GET_NAMESPACES } from '../constants'; + +const initialState = { + namespaces: [], +}; + +const getNamespaces = params => dispatch => + request.get('v1/console/namespaces', { params }).then(response => { + const { code, data } = response; + dispatch({ + type: GET_NAMESPACES, + data: code === 200 ? data : [], + }); + }); + +export default (state = initialState, action) => { + switch (action.type) { + case GET_NAMESPACES: + return { ...state, namespaces: action.data }; + default: + return state; + } +}; + +export { getNamespaces };