diff --git a/auth/pom.xml b/auth/pom.xml index 368d2d0cc..5f70b6263 100644 --- a/auth/pom.xml +++ b/auth/pom.xml @@ -45,11 +45,26 @@ nacos-sys + + org.springframework.boot + spring-boot-starter-security + + org.springframework.boot spring-boot-starter true + + + org.springframework.boot + spring-boot-starter-jdbc + + + + mysql + mysql-connector-java + org.apache.tomcat.embed diff --git a/auth/src/main/java/com/alibaba/nacos/auth/AuthService.java b/auth/src/main/java/com/alibaba/nacos/auth/AuthService.java index 8d2655249..b4650aecb 100644 --- a/auth/src/main/java/com/alibaba/nacos/auth/AuthService.java +++ b/auth/src/main/java/com/alibaba/nacos/auth/AuthService.java @@ -41,8 +41,9 @@ public interface AuthService { * @param identityContext where we can find the user information. * @param permission permission to auth. * @return Boolean if the user has the resource authority. + * @throws AccessException authority authentication error. */ - Boolean authorityAccess(IdentityContext identityContext, Permission permission); + Boolean authorityAccess(IdentityContext identityContext, Permission permission) throws AccessException; /** * AuthService Name which for conveniently find AuthService instance. diff --git a/auth/src/main/java/com/alibaba/nacos/auth/JwtAuthenticationEntryPoint.java b/auth/src/main/java/com/alibaba/nacos/auth/JwtAuthenticationEntryPoint.java new file mode 100644 index 000000000..0bba7fba5 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/JwtAuthenticationEntryPoint.java @@ -0,0 +1,46 @@ +/* + * Copyright 1999-2021 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.auth; + +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/auth/src/main/java/com/alibaba/nacos/auth/JwtTokenManager.java b/auth/src/main/java/com/alibaba/nacos/auth/JwtTokenManager.java new file mode 100644 index 000000000..5751408f0 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/JwtTokenManager.java @@ -0,0 +1,103 @@ +/* + * Copyright 1999-2021 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.auth; + +import com.alibaba.nacos.auth.common.AuthConfigs; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +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.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()); + } + + /** + * Create token. + * + * @param userName auth info + * @return token + */ + public String createToken(String userName) { + + long now = System.currentTimeMillis(); + + Date validity; + validity = new Date(now + authConfigs.getTokenValidityInSeconds() * 1000L); + + Claims claims = Jwts.claims().setSubject(userName); + return Jwts.builder().setClaims(claims).setExpiration(validity) + .signWith(Keys.hmacShaKeyFor(authConfigs.getSecretKeyBytes()), SignatureAlgorithm.HS256).compact(); + } + + /** + * Get auth Info. + * + * @param token token + * @return auth info + */ + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parserBuilder().setSigningKey(authConfigs.getSecretKeyBytes()).build() + .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 + */ + public void validateToken(String token) { + Jwts.parserBuilder().setSigningKey(authConfigs.getSecretKeyBytes()).build().parseClaimsJws(token); + } + +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/LdapAuthenticationProvider.java b/auth/src/main/java/com/alibaba/nacos/auth/LdapAuthenticationProvider.java new file mode 100644 index 000000000..f90bf1fdb --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/LdapAuthenticationProvider.java @@ -0,0 +1,169 @@ +/* + * 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.auth; + +import com.alibaba.nacos.auth.roles.NacosAuthRoleServiceImpl; +import com.alibaba.nacos.auth.roles.RoleInfo; +import com.alibaba.nacos.auth.users.NacosUserDetails; +import com.alibaba.nacos.auth.users.NacosAuthUserDetailsServiceImpl; +import com.alibaba.nacos.auth.users.User; +import com.alibaba.nacos.auth.util.PasswordEncoderUtil; +import com.alibaba.nacos.common.utils.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +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.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; + +import javax.naming.CommunicationException; +import javax.naming.Context; +import javax.naming.directory.DirContext; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapContext; +import java.util.Hashtable; +import java.util.List; + +import static com.alibaba.nacos.auth.roles.NacosAuthRoleServiceImpl.GLOBAL_ADMIN_ROLE; + +/** + * LDAP auth provider. + * + * @author zjw + */ +@Component +public class LdapAuthenticationProvider implements AuthenticationProvider { + + private static final Logger LOG = LoggerFactory.getLogger(LdapAuthenticationProvider.class); + + private static final String FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; + + private static final String TIMEOUT = "com.sun.jndi.ldap.connect.timeout"; + + private static final String DEFAULT_PASSWORD = "nacos"; + + private static final String LDAP_PREFIX = "LDAP_"; + + private static final String DEFAULT_SECURITY_AUTH = "simple"; + + @Autowired + private NacosAuthUserDetailsServiceImpl userDetailsService; + + @Autowired + private NacosAuthRoleServiceImpl nacosRoleService; + + @Value("${nacos.core.auth.ldap.url:ldap://localhost:389}") + private String ldapUrl; + + @Value("${nacos.core.auth.ldap.timeout:3000}") + private String time; + + @Value("${nacos.core.auth.ldap.userdn:cn={0},ou=user,dc=company,dc=com}") + private String userNamePattern; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + String username = (String) authentication.getPrincipal(); + String password = (String) authentication.getCredentials(); + + if (isAdmin(username)) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (PasswordEncoderUtil.matches(password, userDetails.getPassword())) { + return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()); + } else { + return null; + } + } + + if (!ldapLogin(username, password)) { + return null; + } + + UserDetails userDetails; + try { + userDetails = userDetailsService.loadUserByUsername(LDAP_PREFIX + username); + } catch (UsernameNotFoundException exception) { + String nacosPassword = PasswordEncoderUtil.encode(DEFAULT_PASSWORD); + userDetailsService.createUser(LDAP_PREFIX + username, nacosPassword); + User user = new User(); + user.setUsername(LDAP_PREFIX + username); + user.setPassword(nacosPassword); + userDetails = new NacosUserDetails(user); + } + return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()); + } + + private boolean isAdmin(String username) { + List roleInfos = nacosRoleService.getRoles(username); + if (CollectionUtils.isEmpty(roleInfos)) { + return false; + } + for (RoleInfo roleinfo : roleInfos) { + if (GLOBAL_ADMIN_ROLE.equals(roleinfo.getRole())) { + return true; + } + } + return false; + } + + private boolean ldapLogin(String username, String password) throws AuthenticationException { + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, FACTORY); + env.put(Context.PROVIDER_URL, ldapUrl); + env.put(Context.SECURITY_AUTHENTICATION, DEFAULT_SECURITY_AUTH); + + env.put(Context.SECURITY_PRINCIPAL, userNamePattern.replace("{0}", username)); + env.put(Context.SECURITY_CREDENTIALS, password); + env.put(TIMEOUT, time); + LdapContext ctx = null; + try { + ctx = new InitialLdapContext(env, null); + } catch (CommunicationException e) { + LOG.error("LDAP Service connect timeout:{}", e.getMessage()); + throw new RuntimeException("LDAP Service connect timeout"); + } catch (javax.naming.AuthenticationException e) { + LOG.error("login error:{}", e.getMessage()); + throw new RuntimeException("login error!"); + } catch (Exception e) { + LOG.warn("Exception cause by:{}", e.getMessage()); + return false; + } finally { + closeContext(ctx); + } + return true; + } + + @Override + public boolean supports(Class aClass) { + return aClass.equals(UsernamePasswordAuthenticationToken.class); + } + + private void closeContext(DirContext ctx) { + if (ctx != null) { + try { + ctx.close(); + } catch (Exception e) { + LOG.error("Exception closing context", e); + } + } + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/NacosAuthConfig.java b/auth/src/main/java/com/alibaba/nacos/auth/NacosAuthConfig.java new file mode 100644 index 000000000..f9a75765c --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/NacosAuthConfig.java @@ -0,0 +1,132 @@ +/* + * Copyright 1999-2021 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.auth; + +import com.alibaba.nacos.auth.common.AuthConfigs; +import com.alibaba.nacos.auth.common.AuthSystemTypes; +import com.alibaba.nacos.auth.users.NacosAuthUserDetailsServiceImpl; +import com.alibaba.nacos.common.utils.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +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.web.cors.CorsUtils; + +/** + * Spring security config. + * + * @author Nacos + */ +@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/"; + + public static final String UPDATE_PASSWORD_ENTRY_POINT = CONSOLE_RESOURCE_NAME_PREFIX + "user/password"; + + private static final String DEFAULT_ALL_PATH_PATTERN = "/**"; + + private static final String PROPERTY_IGNORE_URLS = "nacos.security.ignore.urls"; + + @Autowired + private Environment env; + + @Autowired + private JwtTokenManager tokenProvider; + + @Autowired + private AuthConfigs authConfigs; + + @Autowired + private NacosAuthUserDetailsServiceImpl userDetailsService; + + @Autowired + private LdapAuthenticationProvider ldapAuthenticationProvider; + + @Bean(name = BeanIds.AUTHENTICATION_MANAGER) + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Override + public void configure(WebSecurity web) { + + String ignoreUrls = null; + if (AuthSystemTypes.NACOS.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())) { + ignoreUrls = DEFAULT_ALL_PATH_PATTERN; + } else if (AuthSystemTypes.LDAP.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())) { + ignoreUrls = DEFAULT_ALL_PATH_PATTERN; + } + if (StringUtils.isBlank(authConfigs.getNacosAuthSystemType())) { + ignoreUrls = env.getProperty(PROPERTY_IGNORE_URLS, DEFAULT_ALL_PATH_PATTERN); + } + if (StringUtils.isNotBlank(ignoreUrls)) { + for (String each : ignoreUrls.trim().split(SECURITY_IGNORE_URLS_SPILT_CHAR)) { + web.ignoring().antMatchers(each.trim()); + } + } + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + if (AuthSystemTypes.NACOS.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())) { + auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); + } else if (AuthSystemTypes.LDAP.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())) { + auth.authenticationProvider(ldapAuthenticationProvider); + } + } + + @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(); + } + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/NacosAuthServiceImpl.java b/auth/src/main/java/com/alibaba/nacos/auth/NacosAuthServiceImpl.java new file mode 100644 index 000000000..11f0d84fa --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/NacosAuthServiceImpl.java @@ -0,0 +1,144 @@ +/* + * Copyright 1999-2021 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.auth; + +import com.alibaba.nacos.api.common.Constants; +import com.alibaba.nacos.auth.context.IdentityContext; +import com.alibaba.nacos.auth.exception.AccessException; +import com.alibaba.nacos.auth.model.Permission; +import com.alibaba.nacos.auth.roles.NacosAuthRoleServiceImpl; +import com.alibaba.nacos.auth.roles.RoleInfo; +import com.alibaba.nacos.common.utils.StringUtils; +import io.jsonwebtoken.ExpiredJwtException; +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 java.util.List; + +/** + * Builtin access control entry of Nacos. + * + * @author wuyfee + */ +@Component +public class NacosAuthServiceImpl implements AuthService { + + private static final String TOKEN_PREFIX = "Bearer "; + + private static final String PARAM_USERNAME = "username"; + + private static final String PARAM_PASSWORD = "password"; + + @Autowired + private JwtTokenManager jwtTokenManager; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private NacosAuthRoleServiceImpl roleService; + + @Override + public IdentityContext login(IdentityContext identityContext) throws AccessException { + String username = (String) identityContext.getParameter(Constants.USERNAME); + String password = (String) identityContext.getParameter(PARAM_PASSWORD); + String finalName; + Authentication authenticate; + try { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, + password); + authenticate = authenticationManager.authenticate(authenticationToken); + } catch (AuthenticationException e) { + throw new AccessException("unknown user!"); + } + + if (null == authenticate || StringUtils.isBlank(authenticate.getName())) { + finalName = username; + } else { + finalName = authenticate.getName(); + } + + String token = jwtTokenManager.createToken(finalName); + SecurityContextHolder.getContext().setAuthentication(jwtTokenManager.getAuthentication(token)); + + IdentityContext authResult = new IdentityContext(); + authResult.setParameter(Constants.USERNAME, finalName); + authResult.setParameter(Constants.ACCESS_TOKEN, token); + authResult.setParameter(Constants.GLOBAL_ADMIN, false); + List roleInfoList = roleService.getRoles(username); + if (roleInfoList != null) { + for (RoleInfo roleInfo : roleInfoList) { + if (roleInfo.getRole().equals(NacosAuthRoleServiceImpl.GLOBAL_ADMIN_ROLE)) { + authResult.setParameter(Constants.GLOBAL_ADMIN, true); + break; + } + } + } + return authResult; + } + + @Override + public Boolean authorityAccess(IdentityContext identityContext, Permission permission) throws AccessException { + String token; + String bearerToken = (String) identityContext.getParameter(NacosAuthConfig.AUTHORIZATION_HEADER); + if (StringUtils.isNotBlank(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) { + token = bearerToken.substring(7); + } else { + token = (String) identityContext.getParameter(Constants.ACCESS_TOKEN); + } + + String username; + if (StringUtils.isBlank(token)) { + username = (String) login(identityContext).getParameter(Constants.USERNAME); + } else { + username = getUsernameFromToken(token); + } + + if (!roleService.hasPermission(username, permission)) { + throw new AccessException("authorization failed!"); + } + return true; + } + + @Override + public String getAuthServiceName() { + return "NacosAuthServiceImpl"; + } + + /** + * get username from token. + */ + private String getUsernameFromToken(String token) throws AccessException { + try { + jwtTokenManager.validateToken(token); + } catch (ExpiredJwtException e) { + throw new AccessException("token expired!"); + } catch (Exception e) { + throw new AccessException("token invalid!"); + } + + Authentication authentication = jwtTokenManager.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + + return authentication.getName(); + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/model/Page.java b/auth/src/main/java/com/alibaba/nacos/auth/model/Page.java new file mode 100644 index 000000000..c8d580fae --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/model/Page.java @@ -0,0 +1,82 @@ +/* + * 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.auth.model; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * Page. + * copy from config module for resolving circular dependency. + */ +public class Page implements Serializable { + + static final long serialVersionUID = -1L; + + /** + * totalCount. + */ + private int totalCount; + + /** + * pageNumber. + */ + private int pageNumber; + + /** + * pagesAvailable. + */ + private int pagesAvailable; + + /** + * pageItems. + */ + private List pageItems = new ArrayList(); + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } + + public void setPagesAvailable(int pagesAvailable) { + this.pagesAvailable = pagesAvailable; + } + + public void setPageItems(List pageItems) { + this.pageItems = pageItems; + } + + public int getTotalCount() { + return totalCount; + } + + public void setTotalCount(int totalCount) { + this.totalCount = totalCount; + } + + public int getPageNumber() { + return pageNumber; + } + + public int getPagesAvailable() { + return pagesAvailable; + } + + public List getPageItems() { + return pageItems; + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/model/PermissionInfo.java b/auth/src/main/java/com/alibaba/nacos/auth/model/PermissionInfo.java new file mode 100644 index 000000000..12c88f84a --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/model/PermissionInfo.java @@ -0,0 +1,69 @@ +/* + * 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.auth.model; + +import java.io.Serializable; + +/** + * PermissionInfo model. + * + * @author nkorange + * @since 1.2.0 + */ +public class PermissionInfo implements Serializable { + + private static final long serialVersionUID = 388813573388837395L; + + /** + * Role name. + */ + private String role; + + /** + * Resource. + */ + private String resource; + + /** + * Action on resource. + */ + private String action; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public String getAction() { + return action; + } + + public void setAction(String action) { + this.action = action; + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/persist/PermissionPersistService.java b/auth/src/main/java/com/alibaba/nacos/auth/persist/PermissionPersistService.java new file mode 100644 index 000000000..a7fabe8d1 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/persist/PermissionPersistService.java @@ -0,0 +1,40 @@ +/* + * 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.auth.persist; + +import com.alibaba.nacos.auth.model.Page; +import com.alibaba.nacos.auth.model.PermissionInfo; + +/** + * Permission CRUD service. + * + * @author nkorange + * @since 1.2.0 + */ +@SuppressWarnings("PMD.AbstractMethodOrInterfaceMethodMustUseJavadocRule") +public interface PermissionPersistService { + + /** + * get the permissions of role by page. + * + * @param role role + * @param pageNo pageNo + * @param pageSize pageSize + * @return permissions page info + */ + Page getPermissions(String role, int pageNo, int pageSize); +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/persist/RolePersistService.java b/auth/src/main/java/com/alibaba/nacos/auth/persist/RolePersistService.java new file mode 100644 index 000000000..810b60f4e --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/persist/RolePersistService.java @@ -0,0 +1,40 @@ +/* + * 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.auth.persist; + +import com.alibaba.nacos.auth.model.Page; +import com.alibaba.nacos.auth.roles.RoleInfo; + +/** + * Role CRUD service. + * + * @author nkorange + * @since 1.2.0 + */ +@SuppressWarnings("PMD.AbstractMethodOrInterfaceMethodMustUseJavadocRule") +public interface RolePersistService { + + /** + * query the user's roles by username. + * + * @param username username + * @param pageNo pageNo + * @param pageSize pageSize + * @return roles page info + */ + Page getRolesByUserName(String username, int pageNo, int pageSize); +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/persist/UserPersistService.java b/auth/src/main/java/com/alibaba/nacos/auth/persist/UserPersistService.java new file mode 100644 index 000000000..d79fc0a16 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/persist/UserPersistService.java @@ -0,0 +1,41 @@ +package com.alibaba.nacos.auth.persist; + +import com.alibaba.nacos.auth.model.Page; +import com.alibaba.nacos.auth.users.User; + +/** + * User CRUD service. + * + * @author nkorange + * @since 1.2.0 + */ +@SuppressWarnings("PMD.AbstractMethodOrInterfaceMethodMustUseJavadocRule") +public interface UserPersistService { + + /** + * create user. + * + * @param username username + * @param password password + */ + void createUser(String username, String password); + + /** + * query user by username. + * + * @param username username + * @return user + */ + User findUserByUsername(String username); + + /** + * get users by page. + * + * @param pageNo pageNo + * @param pageSize pageSize + * @return user page info + */ + Page getUsers(int pageNo, int pageSize); + +} + diff --git a/auth/src/main/java/com/alibaba/nacos/auth/roles/NacosAuthRoleServiceImpl.java b/auth/src/main/java/com/alibaba/nacos/auth/roles/NacosAuthRoleServiceImpl.java new file mode 100644 index 000000000..3596f3c4e --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/roles/NacosAuthRoleServiceImpl.java @@ -0,0 +1,195 @@ +/* + * Copyright 1999-2021 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.auth.roles; + +import com.alibaba.nacos.auth.NacosAuthConfig; +import com.alibaba.nacos.auth.common.AuthConfigs; +import com.alibaba.nacos.auth.model.Page; +import com.alibaba.nacos.auth.model.Permission; +import com.alibaba.nacos.auth.model.PermissionInfo; +import com.alibaba.nacos.auth.persist.PermissionPersistService; +import com.alibaba.nacos.auth.persist.RolePersistService; +import com.alibaba.nacos.common.utils.ConcurrentHashSet; +import com.alibaba.nacos.common.utils.StringUtils; +import io.jsonwebtoken.lang.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * Nacos builtin role service. + * + * @author nkorange + * @since 1.2.0 + */ +@Service +public class NacosAuthRoleServiceImpl { + + public static final Logger LOGGER = LoggerFactory.getLogger(NacosAuthRoleServiceImpl.class); + + public static final String GLOBAL_ADMIN_ROLE = "ROLE_ADMIN"; + + private static final int DEFAULT_PAGE_NO = 1; + + @Autowired + private AuthConfigs authConfigs; + + @Autowired + private RolePersistService rolePersistService; + + @Autowired + private PermissionPersistService permissionPersistService; + + private volatile Set roleSet = new ConcurrentHashSet<>(); + + private volatile Map> roleInfoMap = new ConcurrentHashMap<>(); + + private volatile Map> permissionInfoMap = new ConcurrentHashMap<>(); + + @Scheduled(initialDelay = 5000, fixedDelay = 15000) + private void reload() { + try { + Page roleInfoPage = rolePersistService + .getRolesByUserName(StringUtils.EMPTY, DEFAULT_PAGE_NO, 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, DEFAULT_PAGE_NO, Integer.MAX_VALUE); + tmpPermissionInfoMap.put(role, permissionInfoPage.getPageItems()); + } + + roleSet = tmpRoleSet; + roleInfoMap = tmpRoleInfoMap; + permissionInfoMap = tmpPermissionInfoMap; + } catch (Exception e) { + LOGGER.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) { + //update password + if (NacosAuthConfig.UPDATE_PASSWORD_ENTRY_POINT.equals(permission.getResource())) { + return true; + } + + 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() || roleInfoList == null) { + Page roleInfoPage = getRolesFromDatabase(username, DEFAULT_PAGE_NO, 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() || permissionInfoList == null) { + Page permissionInfoPage = getPermissionsFromDatabase(role, DEFAULT_PAGE_NO, + Integer.MAX_VALUE); + if (permissionInfoPage != null) { + permissionInfoList = permissionInfoPage.getPageItems(); + } + } + return permissionInfoList; + } + + public Page getPermissionsFromDatabase(String role, int pageNo, int pageSize) { + Page pageInfo = permissionPersistService.getPermissions(role, pageNo, pageSize); + if (pageInfo == null) { + return new Page<>(); + } + return pageInfo; + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/roles/RoleInfo.java b/auth/src/main/java/com/alibaba/nacos/auth/roles/RoleInfo.java new file mode 100644 index 000000000..8c3e050e2 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/roles/RoleInfo.java @@ -0,0 +1,55 @@ +/* + * 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.auth.roles; + +import java.io.Serializable; + +/** + * Role Info. + * + * @author nkorange + * @since 1.2.0 + */ +public class RoleInfo implements Serializable { + + private static final long serialVersionUID = 5946986388047856568L; + + private String role; + + private String username; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String toString() { + return "RoleInfo{" + "role='" + role + '\'' + ", username='" + username + '\'' + '}'; + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/users/NacosAuthUserDetailsServiceImpl.java b/auth/src/main/java/com/alibaba/nacos/auth/users/NacosAuthUserDetailsServiceImpl.java new file mode 100644 index 000000000..fa9a232a0 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/users/NacosAuthUserDetailsServiceImpl.java @@ -0,0 +1,103 @@ +/* + * Copyright 1999-2021 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.auth.users; + +import com.alibaba.nacos.auth.common.AuthConfigs; +import com.alibaba.nacos.auth.model.Page; +import com.alibaba.nacos.auth.persist.UserPersistService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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; + +/** + * Custom user service. + * + * @author wfnuser + * @author nkorange + */ +@Service +public class NacosAuthUserDetailsServiceImpl implements UserDetailsService { + + public static final Logger LOGGER = LoggerFactory.getLogger(NacosAuthUserDetailsServiceImpl.class); + + 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) { + LOGGER.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 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 == null) { + user = getUserFromDatabase(username); + } + return user; + } + + public void createUser(String username, String password) { + userPersistService.createUser(username, password); + } + + public User getUserFromDatabase(String username) { + return userPersistService.findUserByUsername(username); + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/users/NacosUserDetails.java b/auth/src/main/java/com/alibaba/nacos/auth/users/NacosUserDetails.java new file mode 100644 index 000000000..2cb43a6a8 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/users/NacosUserDetails.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-2021 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.auth.users; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +/** + * custom user. + * + * @author wfnuser + */ +public class NacosUserDetails implements UserDetails { + + private final 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; + } +} \ No newline at end of file diff --git a/auth/src/main/java/com/alibaba/nacos/auth/users/User.java b/auth/src/main/java/com/alibaba/nacos/auth/users/User.java new file mode 100644 index 000000000..b42728ecd --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/users/User.java @@ -0,0 +1,49 @@ +/* + * 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.auth.users; + +import java.io.Serializable; + +/** + * User. + * + * @author wfnuser + */ +public class User implements Serializable { + + private static final long serialVersionUID = 3371769277802700069L; + + private String username; + + private String password; + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/auth/src/main/java/com/alibaba/nacos/auth/util/PasswordEncoderUtil.java b/auth/src/main/java/com/alibaba/nacos/auth/util/PasswordEncoderUtil.java new file mode 100644 index 000000000..a81723362 --- /dev/null +++ b/auth/src/main/java/com/alibaba/nacos/auth/util/PasswordEncoderUtil.java @@ -0,0 +1,35 @@ +/* + * Copyright 1999-2021 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.auth.util; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +/** + * Password encoder tool. + * + * @author nacos + */ +public class PasswordEncoderUtil { + + public static Boolean matches(String raw, String encoded) { + return new BCryptPasswordEncoder().matches(raw, encoded); + } + + public static String encode(String raw) { + return new BCryptPasswordEncoder().encode(raw); + } +} diff --git a/auth/src/test/java/com/alibaba/nacos/auth/common/AuthPluginManagerTest.java b/auth/src/test/java/com/alibaba/nacos/auth/common/AuthPluginManagerTest.java index d2bcabb75..c4f23282b 100644 --- a/auth/src/test/java/com/alibaba/nacos/auth/common/AuthPluginManagerTest.java +++ b/auth/src/test/java/com/alibaba/nacos/auth/common/AuthPluginManagerTest.java @@ -19,6 +19,7 @@ package com.alibaba.nacos.auth.common; import com.alibaba.nacos.auth.AuthPluginManager; import com.alibaba.nacos.auth.AuthService; import com.alibaba.nacos.auth.context.IdentityContext; +import com.alibaba.nacos.auth.exception.AccessException; import com.alibaba.nacos.auth.model.Permission; import org.junit.Assert; import org.junit.Before; @@ -72,7 +73,7 @@ public class AuthPluginManagerTest { } @Test - public void testFindAuthServiceSpiImpl() { + public void testFindAuthServiceSpiImpl() throws AccessException { Mockito.when(authService.authorityAccess(identityContext, permission)).thenReturn(true); Mockito.when(authService.getAuthServiceName()).thenReturn(TYPE); Optional authServiceImpl = authPluginManager.findAuthServiceSpiImpl(TYPE);