From 2837118ed87e18d5a1ce2d42fc38bbe4bf48bd23 Mon Sep 17 00:00:00 2001 From: z15lross Date: Tue, 9 Nov 2021 14:40:38 +0100 Subject: [PATCH] feat: rbac --- code/api/api/pom.xml | 19 + .../main/java/com/decathlon/ara/Messages.java | 3 + .../com/decathlon/ara/cache/CacheService.java | 104 ++ .../security/CustomSecurity.java | 340 ++++- .../decathlon/ara/service/DemoService.java | 35 +- .../ara/service/GroupMemberService.java | 43 + .../decathlon/ara/service/GroupService.java | 78 ++ .../decathlon/ara/service/MemberService.java | 117 ++ .../service/ProjectGroupMemberService.java | 55 + .../ara/service/ProjectMemberService.java | 18 + .../decathlon/ara/service/ProjectService.java | 106 +- .../ara/service/ProjectUserMemberService.java | 43 + .../ara/service/UserPreferenceService.java | 55 + .../decathlon/ara/service/UserService.java | 33 + .../ara/service/auditing/AuditingService.java | 60 + .../dto/auditing/MemberRoleDetails.java | 23 + .../dto/auditing/ProjectRoleDetails.java | 30 + .../service/dto/auditing/UserRoleDetails.java | 47 + .../ara/service/dto/group/GroupDTO.java | 23 + .../ara/service/dto/member/MemberDTO.java | 30 + .../ara/service/dto/project/ProjectDTO.java | 8 + .../ara/service/dto/user/UserDTO.java | 26 + .../ara/service/security/SecurityService.java | 107 ++ .../ara/web/rest/FunctionalityResource.java | 2 +- .../ara/web/rest/GroupMemberResource.java | 29 + .../decathlon/ara/web/rest/GroupResource.java | 56 + .../ara/web/rest/MemberResource.java | 62 + .../rest/ProjectAdministrationResource.java | 32 + .../web/rest/ProjectGroupMemberResource.java | 20 + .../ara/web/rest/ProjectMemberResource.java | 24 + .../ara/web/rest/ProjectResource.java | 13 +- .../web/rest/ProjectUserMemberResource.java | 20 + .../ara/web/rest/TemplateResource.java | 2 +- .../ara/web/rest/audits/AuditingResource.java | 30 + .../web/rest/authentication/UserResource.java | 46 +- .../ara/authentication/UserResourceTest.java | 59 - .../decathlon/ara/cache/CacheServiceTest.java | 227 ++++ .../decathlon/ara/cache/CacheableTest.java | 1006 +++++++++++++++ .../security/CustomSecurityTest.java | 1093 +++++++++++++++++ .../security/TestAuthentication.java | 77 ++ .../ara/service/DemoServiceTest.java | 57 +- .../ara/service/GroupMemberServiceTest.java | 81 ++ .../ara/service/GroupServiceTest.java | 137 +++ .../ara/service/MemberServiceTest.java | 159 +++ .../ProjectGroupMemberServiceTest.java | 81 ++ .../service/ProjectUserMemberServiceTest.java | 81 ++ .../service/UserPreferenceServiceTest.java | 174 +++ .../ara/service/UserServiceTest.java | 62 + .../service/auditing/AuditingServiceTest.java | 164 +++ .../service/security/SecurityServiceTest.java | 287 +++++ .../java/com/decathlon/ara/util/TestUtil.java | 2 +- .../ara/web/rest/GroupMemberResourceTest.java | 41 + .../ara/web/rest/GroupResourceTest.java | 79 ++ .../ara/web/rest/MemberResourceTest.java | 126 ++ .../rest/ProjectGroupMemberResourceTest.java | 41 + .../ara/web/rest/ProjectResourceIT.java | 212 +--- .../rest/ProjectUserMemberResourceTest.java | 41 + .../rest/authentication/UserResourceTest.java | 82 ++ code/api/api/src/test/resources/data.sql | 4 +- .../oauth2/mappings/oauth2-mappings.json | 28 + .../wiremock/oidc/mappings/oidc-mappings.json | 28 + .../java/com/decathlon/ara/domain/Group.java | 34 + .../com/decathlon/ara/domain/GroupMember.java | 100 ++ .../java/com/decathlon/ara/domain/Member.java | 6 + .../ara/domain/MemberContainerRepository.java | 11 + .../ara/domain/MemberRelationship.java | 16 + .../com/decathlon/ara/domain/Project.java | 32 +- .../ara/domain/ProjectGroupMember.java | 18 + .../decathlon/ara/domain/ProjectMember.java | 94 ++ .../ara/domain/ProjectUserMember.java | 18 + .../java/com/decathlon/ara/domain/User.java | 44 + .../decathlon/ara/domain/UserPreference.java | 70 ++ .../com/decathlon/ara/domain/UserRole.java | 63 + .../ara/domain/enumeration/MemberRole.java | 27 + .../ara/domain/enumeration/Permission.java | 7 + .../domain/enumeration/UserSecurityRole.java | 19 + .../ara/repository/GroupMemberRepository.java | 32 + .../ara/repository/GroupRepository.java | 23 + .../MemberRelationshipRepository.java | 19 + .../ara/repository/MemberRepository.java | 13 + .../ProjectGroupMemberRepository.java | 31 + .../repository/ProjectMemberRepository.java | 32 + .../ara/repository/ProjectRepository.java | 15 +- .../ProjectUserMemberRepository.java | 22 + .../repository/UserPreferenceRepository.java | 14 + .../ara/repository/UserRepository.java | 17 + .../ara/repository/UserRoleRepository.java | 19 + .../h2/20210301165639-h2_migration.yaml | 3 +- .../changes/h2/20210618093532-change_id.yaml | 3 +- .../changes/h2/20220504165317-rbac.yaml | 368 ++++++ .../changes/mysql/20220504170942-rbac.yaml | 326 +++++ .../postgresql/20220504170125-rbac.yaml | 200 +++ .../db/changelog/db.changelog-master-h2.yaml | 2 + .../changelog/db.changelog-master-mysql.yaml | 2 + .../db.changelog-master-postgresql.yaml | 2 + .../database/src/main/resources/ehcache.xml | 21 +- code/web-ui/src/libs/api.js | 2 +- code/web-ui/src/views/management-projects.vue | 9 +- 98 files changed, 7521 insertions(+), 381 deletions(-) create mode 100644 code/api/api/src/main/java/com/decathlon/ara/cache/CacheService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/GroupMemberService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/GroupService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/MemberService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/ProjectGroupMemberService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/ProjectMemberService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/ProjectUserMemberService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/UserPreferenceService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/UserService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/auditing/AuditingService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/MemberRoleDetails.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/ProjectRoleDetails.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/UserRoleDetails.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/dto/group/GroupDTO.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/dto/member/MemberDTO.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/dto/user/UserDTO.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/service/security/SecurityService.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupMemberResource.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupResource.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/MemberResource.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectAdministrationResource.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectGroupMemberResource.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectMemberResource.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectUserMemberResource.java create mode 100644 code/api/api/src/main/java/com/decathlon/ara/web/rest/audits/AuditingResource.java delete mode 100644 code/api/api/src/test/java/com/decathlon/ara/authentication/UserResourceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/cache/CacheServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/cache/CacheableTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/configuration/security/CustomSecurityTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/configuration/security/TestAuthentication.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/GroupMemberServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/GroupServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/MemberServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/ProjectGroupMemberServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/ProjectUserMemberServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/UserPreferenceServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/UserServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/auditing/AuditingServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/service/security/SecurityServiceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupMemberResourceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupResourceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/web/rest/MemberResourceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectGroupMemberResourceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectUserMemberResourceTest.java create mode 100644 code/api/api/src/test/java/com/decathlon/ara/web/rest/authentication/UserResourceTest.java create mode 100644 code/api/api/src/test/resources/wiremock/oauth2/mappings/oauth2-mappings.json create mode 100644 code/api/api/src/test/resources/wiremock/oidc/mappings/oidc-mappings.json create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/Group.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/GroupMember.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/Member.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/MemberContainerRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/MemberRelationship.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/ProjectGroupMember.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/ProjectMember.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/ProjectUserMember.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/User.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/UserPreference.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/UserRole.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/MemberRole.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/Permission.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/UserSecurityRole.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/GroupMemberRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/GroupRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/MemberRelationshipRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/MemberRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/ProjectGroupMemberRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/ProjectMemberRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/ProjectUserMemberRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/UserPreferenceRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/UserRepository.java create mode 100644 code/api/database/src/main/java/com/decathlon/ara/repository/UserRoleRepository.java create mode 100644 code/api/database/src/main/resources/db/changelog/changes/h2/20220504165317-rbac.yaml create mode 100644 code/api/database/src/main/resources/db/changelog/changes/mysql/20220504170942-rbac.yaml create mode 100644 code/api/database/src/main/resources/db/changelog/changes/postgresql/20220504170125-rbac.yaml diff --git a/code/api/api/pom.xml b/code/api/api/pom.xml index a0bda9795..0f1ad9caa 100644 --- a/code/api/api/pom.xml +++ b/code/api/api/pom.xml @@ -66,6 +66,18 @@ io.micrometer micrometer-registry-influx + + + + org.springframework.boot + spring-boot-starter-cache + + + + + org.springframework.boot + spring-boot-starter-validation + @@ -96,6 +108,13 @@ spring-boot-devtools true + + + com.github.tomakehurst + wiremock + 2.32.0 + test + diff --git a/code/api/api/src/main/java/com/decathlon/ara/Messages.java b/code/api/api/src/main/java/com/decathlon/ara/Messages.java index b9db452a4..4758d4532 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/Messages.java +++ b/code/api/api/src/main/java/com/decathlon/ara/Messages.java @@ -22,9 +22,12 @@ public final class Messages { private Messages() { } + public static final String ALREADY_EXIST = "The %s already exist"; + public static final String PARAMETER_IS_MISSING = "One or more parameters is missing or null"; public static final String PARAMETER_HAS_ONE_OR_MORE_MISSING_FIELDS = "A parameter has one or more missing field"; + public static final String NOT_FOUND = "The %s does not exist: it has perhaps been removed."; public static final String NOT_FOUND_COMMUNICATION = "The communication does not exist: it has perhaps been removed."; public static final String NOT_FOUND_COUNTRY = "The country does not exist: it has perhaps been removed."; public static final String NOT_FOUND_CYCLE_DEFINITION = "The cycle definition does not exist: it has perhaps been removed."; diff --git a/code/api/api/src/main/java/com/decathlon/ara/cache/CacheService.java b/code/api/api/src/main/java/com/decathlon/ara/cache/CacheService.java new file mode 100644 index 000000000..2f5d8143f --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/cache/CacheService.java @@ -0,0 +1,104 @@ +package com.decathlon.ara.cache; + +import java.util.function.Predicate; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.ehcache.EhCacheCache; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import com.decathlon.ara.domain.Project; + +@Service +public class CacheService { + + private static final String USER_PROJECTS_CACHE_NAME = "security.user.projects"; + private static final String USER_PROJECT_ROLES_CACHE_NAME = "security.user.project.roles"; + + private CacheManager cacheManager; + + public CacheService(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + public void evictCaches(Project project) { + evictCacheAfterCommit(() -> { + evictCache(USER_PROJECT_ROLES_CACHE_NAME, key -> key.toString().startsWith(project.getCode())); + + Cache cache = cacheManager.getCache(USER_PROJECTS_CACHE_NAME); + if (cache != null) { + cache.clear(); + } + }); + } + + public void evictCaches(Project project, String userName) { + evictsUserProjectRolesCache(project, userName); + evictsUserProjectsCache(userName); + } + + public void evictsUserProjectRolesCache(Project project, String userName) { + evictCacheAfterCommit(() -> { + Cache cache = cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME); + if (cache != null) { + cache.evict(project.getCode().concat(userName)); + } + }); + } + + public void evictsUserProjectRolesCache(String userName) { + evictCacheAfterCommit(() -> evictCache(USER_PROJECT_ROLES_CACHE_NAME, key -> key.toString().endsWith(userName))); + } + + public void evictsUserProjectsCache(String userName) { + evictCacheAfterCommit(() -> { + Cache cache = cacheManager.getCache(USER_PROJECTS_CACHE_NAME); + if (cache != null) { + cache.evict(userName); + } + }); + } + + public void evictCaches(String userName) { + evictsUserProjectRolesCache(userName); + evictsUserProjectsCache(userName); + } + + @SuppressWarnings("unchecked") + private void evictCache(String cacheName, Predicate keyPredicate) { + Cache cache = cacheManager.getCache(cacheName); + if (cache != null) { + if (cache instanceof EhCacheCache ehCacheCache) { + ehCacheCache.getNativeCache().getKeys().stream().filter(keyPredicate).forEach(cache::evict); + } else { + cache.clear(); + } + } + } + + private void evictCacheAfterCommit(Runnable evictRunnable) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new EvictCacheSynchronization(evictRunnable)); + } else { + evictRunnable.run(); + } + } + + private static class EvictCacheSynchronization implements TransactionSynchronization { + + private Runnable evictRunnable; + + public EvictCacheSynchronization(Runnable evictRunnable) { + this.evictRunnable = evictRunnable; + } + + @Override + public void afterCommit() { + evictRunnable.run(); + } + + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/configuration/security/CustomSecurity.java b/code/api/api/src/main/java/com/decathlon/ara/configuration/security/CustomSecurity.java index a69155217..4674d8eb6 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/configuration/security/CustomSecurity.java +++ b/code/api/api/src/main/java/com/decathlon/ara/configuration/security/CustomSecurity.java @@ -1,13 +1,57 @@ package com.decathlon.ara.configuration.security; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + import org.apache.logging.log4j.util.Strings; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.util.Assert; + +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.domain.enumeration.Permission; +import com.decathlon.ara.domain.enumeration.UserSecurityRole; +import com.decathlon.ara.loader.DemoLoaderConstants; +import com.decathlon.ara.service.security.SecurityService; +import com.decathlon.ara.web.rest.util.RestConstants; @Configuration +@EnableCaching public class CustomSecurity { @Value("${ara.clientBaseUrl}") @@ -28,29 +72,53 @@ public class CustomSecurity { @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri:}") private String resourceJwkSetUri; + private SecurityService securityService; + + public CustomSecurity(SecurityService securityService) { + this.securityService = securityService; + } + @Bean public SecurityFilterChain configure(HttpSecurity http) throws Exception { // Whatever is the authentication method, we need to authorize /actguatorgw String redirectUrl = String.format("%s?spring_redirect=true", this.clientBaseUrl); - http - .csrf().disable() //NOSONAR - .authorizeRequests() //NOSONAR - .antMatchers("/oauth/**", "/actuator/**").permitAll() - .anyRequest().authenticated() - .and() - .logout() - .logoutUrl(this.logoutProcessingUrl) // logout entrypoint - .invalidateHttpSession(true) - .clearAuthentication(true).logoutSuccessUrl(redirectUrl).deleteCookies("JSESSIONID").permitAll() - .and() - .oauth2Login() - .loginProcessingUrl(this.loginProcessingUrl + "/*") // path to redirect to from auth server - .loginPage(String.format("%s/%s", this.clientBaseUrl, "login")) // standard spring redirection for protected resources - .defaultSuccessUrl(redirectUrl, true) // once logged in, redirect to - .authorizationEndpoint().baseUri(this.loginStartingUrl); // entrypoint to initialize oauth processing + configureProjectAccess(http, + new String[] { "/problems", "/problem-patterns", "/errors", "/executions", "/functionalities" }, + new MethodAccess(UserSecurityRole.ADMIN, Permission.WRITE, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE)); + configureProjectAccess(http, + new MethodAccess(UserSecurityRole.ADMIN, Permission.READ, HttpMethod.GET, HttpMethod.OPTIONS), + new MethodAccess(UserSecurityRole.ADMIN, Permission.ADMIN, HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE)); + configureGroupAccess(http, new MethodAccess(UserSecurityRole.ADMIN, Permission.WRITE, HttpMethod.POST, HttpMethod.PATCH, HttpMethod.DELETE)); + configureAccess(http, new String[] { "/admin/projects" }, + new MethodAccess(UserSecurityRole.ADMIN, HttpMethod.GET)); + configureAccess(http, new String[] { "/demo" }, + methodAccess -> methodAccess.makePermissionAccess(builder -> builder.append("@customSecurity.hasProjectPermission(authentication, '").append(DemoLoaderConstants.PROJECT_CODE_DEMO).append("', '")), + new MethodAccess(UserSecurityRole.ADMIN, Permission.ADMIN, HttpMethod.DELETE)); + configureAccess(http, new String[] { "/projects", "/groups", "/demo" }, + new MethodAccess(UserSecurityRole.PROJECT_OR_GROUP_CREATOR, HttpMethod.POST)); + configureAccess(http, new String[] { "/features" }, + new MethodAccess(UserSecurityRole.ADMIN, HttpMethod.PATCH, HttpMethod.DELETE)); + configureAccess(http, new String[] { "/users/current/details" }, + new MethodAccess(HttpMethod.GET)); + configureAccess(http, new String[] { "/users/{userName}" }, + new MethodAccess(UserSecurityRole.ADMIN, HttpMethod.GET)); + configureAccess(http, new String[] { "/auditing" }, + new MethodAccess(UserSecurityRole.AUDITING, HttpMethod.GET)); + http.csrf().disable() // NOSONAR + .authorizeRequests(requests -> requests + .antMatchers("/oauth/**", "/actuator/**", "/test/**").permitAll().anyRequest().authenticated()) + .logout(logout -> logout.logoutUrl(this.logoutProcessingUrl) // logout entrypoint + .invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl(redirectUrl) + .deleteCookies("JSESSIONID").permitAll()) + .oauth2Login(login -> login.loginProcessingUrl(this.loginProcessingUrl + "/*") // path to redirect to from auth server + .loginPage(String.format("%s/%s", this.clientBaseUrl, "login")) // standard spring redirection for protected resources + .defaultSuccessUrl(redirectUrl, true) // once logged in, redirect to + .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.baseUri(this.loginStartingUrl)) + .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(oauth2UserService(new DefaultOAuth2UserService(), this::constructOAuth2User)) + .oidcUserService(oauth2UserService(new OidcUserService(), this::constructOidcUser)))); // entrypoint to initialize oauth processing if (isResourceServerConfiguration()) { - http.oauth2ResourceServer().jwt(); + http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(this::convert); } return http.build(); } @@ -59,4 +127,242 @@ private boolean isResourceServerConfiguration() { return Strings.isNotBlank(resourceIssuerUri) || Strings.isNotBlank(resourceJwkSetUri); } + private OAuth2UserService oauth2UserService(OAuth2UserService delegate, OAuth2UserConstructor userConstructor) { + return request -> { + U remoteUser = delegate.loadUser(request); + User user = securityService.initUser(remoteUser.getName(), request.getClientRegistration().getProviderDetails().getIssuerUri()); + List authorities = getUserAuthorities(user.getId()); + return userConstructor.constructUser(user.getId(), request, remoteUser, authorities); + }; + } + + private OAuth2User constructOAuth2User(String userName, OAuth2UserRequest request, OAuth2User remoteUser, List authorities) { + return new AraOauth2User(userName, authorities, remoteUser.getAttributes()); + } + + private OidcUser constructOidcUser(String userName, OidcUserRequest request, OidcUser user, List authorities) { + return new AraOidcUser(userName, authorities, user.getIdToken(), user.getUserInfo()); + } + + private AbstractAuthenticationToken convert(Jwt jwt) { + User user = securityService.initUser(jwt.getSubject(), jwt.getClaimAsString(JwtClaimNames.ISS)); + return new JwtAuthenticationToken(jwt, getUserAuthorities(user.getId()), user.getId()); + } + + private List getUserAuthorities(String userId) { + return securityService.getUserRoles(userId).stream().map(userRole -> new SimpleGrantedAuthority(getSecurityRole(userRole))).toList(); + } + + @Bean + public RoleHierarchy roleHierarchy() { + RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); + StringBuilder roleHierarchyBuilder = new StringBuilder(); + Set processedRole = new HashSet<>(); + for (UserSecurityRole role : UserSecurityRole.values()) { + appendHierarchy(roleHierarchyBuilder, processedRole, role); + } + roleHierarchy.setHierarchy(roleHierarchyBuilder.toString()); + return roleHierarchy; + } + + private void appendHierarchy(StringBuilder roleHierarchyBuilder, Set processedRole, UserSecurityRole role) { + UserSecurityRole parent = role.getParent(); + if (parent != null && processedRole.add(role)) { + appendHierarchy(roleHierarchyBuilder, processedRole, parent); + roleHierarchyBuilder.append(getSecurityRole(parent)).append(" > ").append(getSecurityRole(role)).append(" \n"); + } + } + + private static String getSecurityRole(UserSecurityRole role) { + return "ROLE_" + role.name(); + } + + public boolean hasProjectPermission(Authentication authentication, String projectCode, String permission) { + return hasPermission(securityService.getProjectMemberRoles(projectCode, authentication.getName()), permission); + } + + public boolean hasGroupPermission(Authentication authentication, String groupName, String permission) { + return hasPermission(securityService.getGroupMemberRoles(groupName, authentication.getName()), permission); + } + + private boolean hasPermission(Set memberRoles, String permission) { + return memberRoles.stream().anyMatch(projectMemberType -> projectMemberType.hasPermission(Permission.valueOf(permission))); + } + + private void configureAccess(HttpSecurity http, String[] urlPatterns, MethodAccess... accesses) throws Exception { + configureAccess(http, RestConstants.API_PATH, urlPatterns, MethodAccess::makeRoleAccess, accesses); + } + + private void configureAccess(HttpSecurity http, String[] urlPatterns, Function.AuthorizedUrl>> authorizedUrlConfigurer, MethodAccess... accesses) throws Exception { + configureAccess(http, RestConstants.API_PATH, urlPatterns, authorizedUrlConfigurer, accesses); + } + + private void configureGroupAccess(HttpSecurity http, MethodAccess... accesses) throws Exception { + configureAccess(http, RestConstants.API_PATH + "/groups/{groupName}", null, MethodAccess::makeGroupAccess, accesses); + } + + private void configureProjectAccess(HttpSecurity http, MethodAccess... accesses) throws Exception { + configureProjectAccess(http, null, accesses); + } + + private void configureProjectAccess(HttpSecurity http, String[] urlPatterns, MethodAccess... accesses) throws Exception { + configureAccess(http, RestConstants.PROJECT_API_PATH, urlPatterns, MethodAccess::makeProjectAccess, accesses); + } + + private void configureAccess(HttpSecurity http, String urlPatternPrefix, String[] urlPatterns, Function.AuthorizedUrl>> authorizedUrlConfigurer, MethodAccess... accesses) throws Exception { + http.authorizeRequests(requests -> { + for (MethodAccess access : accesses) { + for (HttpMethod method : access.methods()) { + Stream patternStream; + if (urlPatterns == null) { + patternStream = Stream.of(""); + } else { + patternStream = Arrays.stream(urlPatterns); + } + ExpressionUrlAuthorizationConfigurer.AuthorizedUrl antMatchers = requests.antMatchers(method, patternStream.mapMulti((pattern, stream) -> { + Assert.state(!pattern.startsWith(urlPatternPrefix), () -> "when configuring access, pattern must not start with the prefix pattern. Got prefix " + urlPatternPrefix + " and pattern " + pattern); + String projectUriPattern = urlPatternPrefix + pattern; + stream.accept(projectUriPattern); + stream.accept(projectUriPattern + "/**"); + }).toArray(String[]::new)); + authorizedUrlConfigurer.apply(access).accept(antMatchers); + } + } + }); + } + + private static record MethodAccess(UserSecurityRole role, Permission permission, List methods) { + + /** + * Access to httpMethods is not restricted + * @param httpMethods + */ + public MethodAccess(HttpMethod... httpMethods) { + this(null, null, List.of(httpMethods)); + } + + /** + * Access to httpMethods is available for user with the given role + * @param role + * @param httpMethods + */ + public MethodAccess(UserSecurityRole role, HttpMethod... httpMethods) { + this(role, null, List.of(httpMethods)); + } + + /** + * Access to httpMethods is available for user with the given role or the given permission + * @see CustomSecurity#configureProjectAccess(HttpSecurity, MethodAccess...) + * @see CustomSecurity#configureProjectAccess(HttpSecurity, String[], MethodAccess...) + * @see CustomSecurity#configureGroupAccess(HttpSecurity, MethodAccess...) + * @param role + * @param permission + * @param httpMethods + */ + public MethodAccess(UserSecurityRole role, Permission permission, HttpMethod... httpMethods) { + this(role, permission, List.of(httpMethods)); + } + + private Consumer.AuthorizedUrl> makeRoleAccess() { + return authorizedUrl -> { + if (role != null) { + authorizedUrl.hasRole(role.name()); + } else { + authorizedUrl.authenticated(); + } + }; + } + + private Consumer.AuthorizedUrl> makeProjectAccess() { + return makePermissionAccess(builder -> builder.append("@customSecurity.hasProjectPermission(authentication, #projectCode, '")); + } + + private Consumer.AuthorizedUrl> makeGroupAccess() { + return makePermissionAccess(builder -> builder.append("@customSecurity.hasGroupPermission(authentication, #groupName, '")); + } + + private Consumer.AuthorizedUrl> makePermissionAccess(Consumer builderConsumer) { + StringBuilder builder = new StringBuilder(); + builder.append("hasRole('").append(getSecurityRole(role)).append("')").append(" or "); + builderConsumer.accept(builder); + builder.append(permission.name()).append("')"); + return authorizedUrl -> authorizedUrl.access(builder.toString()); + } + + } + + private static class AraOauth2User implements OAuth2User { + + private String name; + private Collection authorities; + private Map attributes; + + public AraOauth2User(String name, Collection authorities, Map attributes) { + this.name = name; + this.authorities = Collections.unmodifiableCollection(authorities); + this.attributes = Collections.unmodifiableMap(attributes); + } + + @Override + public String getName() { + return name; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public Map getAttributes() { + return attributes; + } + } + + private static class AraOidcUser extends DefaultOidcUser { + + private static final long serialVersionUID = 1L; + + private String name; + + public AraOidcUser(String name, Collection authorities, OidcIdToken idToken, OidcUserInfo userInfo) { + super(authorities, idToken, userInfo); + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + Objects.hash(name); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof AraOidcUser)) { + return false; + } + AraOidcUser other = (AraOidcUser) obj; + return Objects.equals(name, other.name); + } + + } + + @FunctionalInterface + private interface OAuth2UserConstructor { + U constructUser(String userName, R request, U remoteUser, List authorities); + } + } diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/DemoService.java b/code/api/api/src/main/java/com/decathlon/ara/service/DemoService.java index 13f698aee..4fc542d41 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/service/DemoService.java +++ b/code/api/api/src/main/java/com/decathlon/ara/service/DemoService.java @@ -23,24 +23,24 @@ import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.Optional; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.decathlon.ara.Entities; -import com.decathlon.ara.Messages; -import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.enumeration.MemberRole; import com.decathlon.ara.loader.DemoExecutionLoader; import com.decathlon.ara.loader.DemoFunctionalityLoader; import com.decathlon.ara.loader.DemoProblemLoader; import com.decathlon.ara.loader.DemoScenarioLoader; import com.decathlon.ara.loader.DemoSettingsLoader; -import com.decathlon.ara.repository.ProjectRepository; import com.decathlon.ara.service.dto.cycledefinition.CycleDefinitionDTO; +import com.decathlon.ara.service.dto.member.MemberDTO; import com.decathlon.ara.service.dto.project.ProjectDTO; import com.decathlon.ara.service.dto.team.TeamDTO; import com.decathlon.ara.service.exception.BadRequestException; @@ -56,8 +56,6 @@ public class DemoService { private static final Logger LOG = LoggerFactory.getLogger(DemoService.class); - private final ProjectRepository projectRepository; - private final ProjectService projectService; private final SettingService settingService; @@ -72,12 +70,13 @@ public class DemoService { private final DemoSettingsLoader demoSettingsLoader; + private final ProjectUserMemberService projectUserMemberService; + @Autowired - public DemoService(ProjectRepository projectRepository, ProjectService projectService, - SettingService settingService, DemoExecutionLoader demoExecutionLoader, + public DemoService(ProjectService projectService, SettingService settingService, DemoExecutionLoader demoExecutionLoader, DemoFunctionalityLoader demoFunctionalityLoader, DemoProblemLoader demoProblemLoader, - DemoScenarioLoader demoScenarioLoader, DemoSettingsLoader demoSettingsLoader) { - this.projectRepository = projectRepository; + DemoScenarioLoader demoScenarioLoader, DemoSettingsLoader demoSettingsLoader, + ProjectUserMemberService projectUserMemberService) { this.projectService = projectService; this.settingService = settingService; this.demoExecutionLoader = demoExecutionLoader; @@ -85,6 +84,7 @@ public DemoService(ProjectRepository projectRepository, ProjectService projectSe this.demoProblemLoader = demoProblemLoader; this.demoScenarioLoader = demoScenarioLoader; this.demoSettingsLoader = demoSettingsLoader; + this.projectUserMemberService = projectUserMemberService; } /** @@ -95,8 +95,10 @@ public DemoService(ProjectRepository projectRepository, ProjectService projectSe */ @Transactional public ProjectDTO create() throws BadRequestException { - if (projectService.findOne(PROJECT_CODE_DEMO).isPresent()) { - throw new BadRequestException(Messages.RULE_DEMO_PROJECT_ALREADY_EXISTS, Entities.PROJECT, "demo-exists"); + Optional existingProject = projectService.findOne(PROJECT_CODE_DEMO); + if (existingProject.isPresent()) { + projectUserMemberService.addMember(PROJECT_CODE_DEMO, new MemberDTO(SecurityContextHolder.getContext().getAuthentication().getName(), MemberRole.ADMIN)); + return existingProject.get(); } // IMPORTANT: Use DTO constructors everywhere, @@ -129,15 +131,12 @@ public ProjectDTO create() throws BadRequestException { * @throws NotFoundException if there is no demo project */ public void delete() throws NotFoundException { - final Project project = projectRepository.findOneByCode(PROJECT_CODE_DEMO); - if (project == null) { - throw new NotFoundException(Messages.NOT_FOUND_PROJECT, Entities.PROJECT); - } + long projectId = projectService.toId(PROJECT_CODE_DEMO); - final String executionBasePath = settingService.get(project.getId().longValue(), + final String executionBasePath = settingService.get(projectId, Settings.EXECUTION_INDEXER_FILE_EXECUTION_BASE_PATH); - projectRepository.delete(project); + projectService.delete(PROJECT_CODE_DEMO); if (executionBasePath.contains(Settings.PROJECT_VARIABLE)) { final String projectExecutionsFolder = executionBasePath diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/GroupMemberService.java b/code/api/api/src/main/java/com/decathlon/ara/service/GroupMemberService.java new file mode 100644 index 000000000..132bd52df --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/GroupMemberService.java @@ -0,0 +1,43 @@ +package com.decathlon.ara.service; + +import org.springframework.stereotype.Service; + +import com.decathlon.ara.cache.CacheService; +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.GroupRepository; +import com.decathlon.ara.repository.UserRepository; + +@Service +public class GroupMemberService extends MemberService { + + private CacheService cacheService; + + public GroupMemberService(GroupRepository groupRepository, GroupMemberRepository groupMemberRepository, UserRepository userRepository, CacheService cacheService) { + super(groupRepository, groupMemberRepository, userRepository); + this.cacheService = cacheService; + } + + @Override + protected GroupMember constructMember(Group group, User member) { + return new GroupMember(group, member); + } + + @Override + protected void afterAddMember(Group group, User member) { + cacheService.evictCaches(member.getMemberName()); + } + + @Override + protected void afterUpdateRole(Group group, User member) { + cacheService.evictsUserProjectRolesCache(member.getMemberName()); + } + + @Override + protected void afterDeleteMember(Group group, User member) { + cacheService.evictCaches(member.getMemberName()); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/GroupService.java b/code/api/api/src/main/java/com/decathlon/ara/service/GroupService.java new file mode 100644 index 000000000..13e7ef94e --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/GroupService.java @@ -0,0 +1,78 @@ +package com.decathlon.ara.service; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import com.decathlon.ara.Messages; +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.GroupRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.dto.group.GroupDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; +import com.decathlon.ara.service.exception.NotUniqueException; + +@Service +public class GroupService { + + private static final String NAME_FOR_MESSAGE = "group"; + + private GroupRepository groupRepository; + private UserRepository userRepository; + private GroupMemberRepository groupMemberRepository; + private ProjectGroupMemberRepository projectGroupMemberRepository; + + public GroupService(GroupRepository groupRepository, UserRepository userRepository, GroupMemberRepository groupMemberRepository, ProjectGroupMemberRepository projectGroupMemberRepository) { + this.groupRepository = groupRepository; + this.userRepository = userRepository; + this.groupMemberRepository = groupMemberRepository; + this.projectGroupMemberRepository = projectGroupMemberRepository; + } + + public List findAll() { + return groupRepository.findAll().stream().map(group -> new GroupDTO(group.getMemberName())).toList(); + } + + public GroupDTO findOne(String name) throws NotFoundException { + Group group = groupRepository.findByMemberName(name); + if (group == null) { + throw new NotFoundException(String.format(Messages.NOT_FOUND, NAME_FOR_MESSAGE), NAME_FOR_MESSAGE); + } + return new GroupDTO(group.getMemberName()); + } + + public GroupDTO create(GroupDTO groupDto) throws NotUniqueException { + Group group = groupRepository.findByMemberName(groupDto.getName()); + if (group != null) { + throw new NotUniqueException(String.format(Messages.ALREADY_EXIST, NAME_FOR_MESSAGE), NAME_FOR_MESSAGE, "name", group.getName()); + } + group = new Group(groupDto.getName()); + groupRepository.save(group); + GroupMember groupMember = new GroupMember(group, userRepository.findByMemberName(SecurityContextHolder.getContext().getAuthentication().getName())); + groupMember.setRole(MemberRole.ADMIN); + groupMemberRepository.save(groupMember); + return groupDto; + } + + public void delete(String groupName) throws BadRequestException { + Group group = groupRepository.findByMemberName(groupName); + if (group == null) { + throw new NotFoundException(String.format(Messages.NOT_FOUND, NAME_FOR_MESSAGE), NAME_FOR_MESSAGE); + } + List projectGroupMemberList = projectGroupMemberRepository.findAllByIdMemberName(groupName); + if (!projectGroupMemberList.isEmpty()) { + throw new BadRequestException("Group cannot be deleted, because it is actually member of all these projects : " + projectGroupMemberList.stream().map(projectGroupMember -> projectGroupMember.getProject().getCode()).collect(Collectors.joining(", ")), NAME_FOR_MESSAGE, "member_of_project"); + } + groupMemberRepository.deleteByIdGroupName(groupName); + groupRepository.delete(group); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/MemberService.java b/code/api/api/src/main/java/com/decathlon/ara/service/MemberService.java new file mode 100644 index 000000000..b0ca040c1 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/MemberService.java @@ -0,0 +1,117 @@ +package com.decathlon.ara.service; + +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; + +import org.springframework.core.GenericTypeResolver; + +import com.decathlon.ara.Messages; +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.MemberContainerRepository; +import com.decathlon.ara.domain.MemberRelationship; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.MemberRelationshipRepository; +import com.decathlon.ara.repository.MemberRepository; +import com.decathlon.ara.service.dto.member.MemberDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; + +public abstract class MemberService> { + + private static final Pattern CAMEL_CASE_WORD_FORMAT_PATTERN = Pattern.compile("([A-Za-z])([A-Z]+)"); + + private MemberContainerRepository memberContainerRepository; + private MemberRelationshipRepository memberRelationshipRepository; + private MemberRepository memberRepository; + + private Class containerClass; + private Class memberClass; + private Class relationshipClass; + + @SuppressWarnings("unchecked") + protected MemberService(MemberContainerRepository memberContainerRepository, MemberRelationshipRepository memberRelationshipRepository, MemberRepository memberRepository) { + this.memberContainerRepository = memberContainerRepository; + this.memberRelationshipRepository = memberRelationshipRepository; + this.memberRepository = memberRepository; + Class[] typeArgumentResolved = GenericTypeResolver.resolveTypeArguments(this.getClass(), MemberService.class); + Objects.requireNonNull(typeArgumentResolved); + containerClass = (Class) typeArgumentResolved[0]; + memberClass = (Class) typeArgumentResolved[1]; + relationshipClass = (Class) typeArgumentResolved[2]; + } + + public List findAll(String identifier) { + return memberRelationshipRepository.findAllByContainerIdentifier(identifier).stream().map(memberRelationship -> new MemberDTO(memberRelationship.getMemberName(), memberRelationship.getRole())).toList(); + } + + public MemberDTO findOne(String identifier, String memberName) throws NotFoundException { + R memberRelationship = memberRelationshipRepository.findByContainerIdentifierAndMemberName(identifier, memberName); + if (memberRelationship == null) { + throw new NotFoundException(notFoundMessage(relationshipClass), makeIdentifier(relationshipClass)); + } + return new MemberDTO(memberRelationship.getMemberName(), memberRelationship.getRole()); + } + + public MemberDTO addMember(String identifier, MemberDTO memberDto) throws BadRequestException { + R memberRelationship = memberRelationshipRepository.findByContainerIdentifierAndMemberName(identifier, memberDto.getName()); + if (memberRelationship == null) { + C container = memberContainerRepository.findByContainerIdentifier(identifier); + if (container == null) { + throw new NotFoundException(notFoundMessage(containerClass), makeIdentifier(containerClass)); + } + M member = memberRepository.findByMemberName(memberDto.getName()); + if (member == null) { + throw new NotFoundException(notFoundMessage(memberClass), makeIdentifier(memberClass)); + } + memberRelationship = constructMember(container, member); + } else { + throw new BadRequestException(String.format(Messages.ALREADY_EXIST, relationshipClass), makeIdentifier(relationshipClass), "already_exists"); + } + memberRelationship.setRole(memberDto.getRole()); + memberRelationshipRepository.save(memberRelationship); + afterAddMember(memberRelationship.getContainer(), memberRelationship.getMember()); + return memberDto; + } + + public MemberDTO updateMemberRole(String identifier, String memberName, MemberRole role) throws BadRequestException { + R memberRelationship = memberRelationshipRepository.findByContainerIdentifierAndMemberName(identifier, memberName); + if (memberRelationship == null) { + throw new NotFoundException(notFoundMessage(relationshipClass), makeIdentifier(relationshipClass)); + } + memberRelationship.setRole(role); + memberRelationshipRepository.save(memberRelationship); + afterUpdateRole(memberRelationship.getContainer(), memberRelationship.getMember()); + return new MemberDTO(memberName, role); + } + + public void deleteMember(String identifier, String memberName) throws BadRequestException { + R memberRelationship = memberRelationshipRepository.findByContainerIdentifierAndMemberName(identifier, memberName); + if (memberRelationship == null) { + throw new NotFoundException(notFoundMessage(relationshipClass), makeIdentifier(relationshipClass)); + } + memberRelationshipRepository.delete(memberRelationship); + afterDeleteMember(memberRelationship.getContainer(), memberRelationship.getMember()); + } + + protected abstract R constructMember(C container, M member); + + protected abstract void afterAddMember(C container, M member); + + protected abstract void afterUpdateRole(C container, M member); + + protected abstract void afterDeleteMember(C container, M member); + + private String notFoundMessage(Class entityClass) { + return String.format(Messages.NOT_FOUND, toStringForMessage(entityClass)); + } + + private String makeIdentifier(Class entityClass) { + return CAMEL_CASE_WORD_FORMAT_PATTERN.matcher(entityClass.getSimpleName()).replaceAll("$1-$2").toLowerCase(); + } + + private String toStringForMessage(Class entityClass) { + return CAMEL_CASE_WORD_FORMAT_PATTERN.matcher(entityClass.getSimpleName()).replaceAll("$1 $2").toLowerCase(); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/ProjectGroupMemberService.java b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectGroupMemberService.java new file mode 100644 index 000000000..69f3846f7 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectGroupMemberService.java @@ -0,0 +1,55 @@ +package com.decathlon.ara.service; + +import java.util.function.BiConsumer; + +import org.springframework.stereotype.Service; + +import com.decathlon.ara.cache.CacheService; +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.GroupRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.ProjectRepository; + +@Service +public class ProjectGroupMemberService extends ProjectMemberService { + + private GroupMemberRepository groupMemberRepository; + private CacheService cacheService; + + public ProjectGroupMemberService(ProjectRepository projectRepository, ProjectGroupMemberRepository projectGroupMemberRepository, GroupRepository groupRepository, GroupMemberRepository groupMemberRepository, CacheService cacheService) { + super(projectRepository, projectGroupMemberRepository, groupRepository); + this.groupMemberRepository = groupMemberRepository; + this.cacheService = cacheService; + } + + @Override + protected ProjectGroupMember constructMember(Project project, Group member) { + return new ProjectGroupMember(project, member); + } + + @Override + protected void afterAddMember(Project project, Group member) { + evictCaches(project, member, cacheService::evictCaches); + } + + @Override + protected void afterUpdateRole(Project project, Group member) { + evictCaches(project, member, cacheService::evictsUserProjectRolesCache); + } + + @Override + protected void afterDeleteMember(Project project, Group member) { + evictCaches(project, member, cacheService::evictCaches); + } + + private void evictCaches(Project project, Group member, BiConsumer groupMemberEvictCache) { + for (GroupMember groupMember : groupMemberRepository.findAllByIdGroupName(member.getName())) { + groupMemberEvictCache.accept(project, groupMember.getMemberName()); + } + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/ProjectMemberService.java b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectMemberService.java new file mode 100644 index 000000000..bdac5d38a --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectMemberService.java @@ -0,0 +1,18 @@ +package com.decathlon.ara.service; + +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectMember; +import com.decathlon.ara.repository.MemberRepository; +import com.decathlon.ara.repository.ProjectMemberRepository; +import com.decathlon.ara.repository.ProjectRepository; + +public abstract class ProjectMemberService> extends MemberService { + + static final String CACHE_NAME = "security.user.project.roles"; + + protected ProjectMemberService(ProjectRepository projectRepository, ProjectMemberRepository projectMemberRepository, MemberRepository memberRepository) { + super(projectRepository, projectMemberRepository, memberRepository); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/ProjectService.java b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectService.java index c576ea285..8d200a807 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/service/ProjectService.java +++ b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectService.java @@ -18,18 +18,30 @@ package com.decathlon.ara.service; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.decathlon.ara.Entities; import com.decathlon.ara.Messages; +import com.decathlon.ara.cache.CacheService; import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectUserMember; import com.decathlon.ara.domain.RootCause; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; import com.decathlon.ara.repository.ProjectRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; import com.decathlon.ara.repository.RootCauseRepository; +import com.decathlon.ara.repository.UserRepository; import com.decathlon.ara.service.dto.project.ProjectDTO; import com.decathlon.ara.service.exception.BadRequestException; import com.decathlon.ara.service.exception.NotFoundException; @@ -47,16 +59,31 @@ public class ProjectService { private final RootCauseRepository rootCauseRepository; + private final UserRepository userRepository; + + private final ProjectUserMemberRepository projectUserMemberRepository; + + private final ProjectGroupMemberRepository projectGroupMemberRepository; + private final GenericMapper mapper; private final CommunicationService communicationService; - public ProjectService(ProjectRepository repository, RootCauseRepository rootCauseRepository, GenericMapper mapper, - CommunicationService communicationService) { + private final UserPreferenceService userPreferenceService; + + private final CacheService cacheService; + + @Autowired + public ProjectService(ProjectRepository repository, RootCauseRepository rootCauseRepository, UserRepository userRepository, ProjectUserMemberRepository projectUserMemberRepository, ProjectGroupMemberRepository projectGroupMemberRepository, GenericMapper mapper, CommunicationService communicationService, UserPreferenceService userPreferenceService, CacheService cacheService) { this.repository = repository; this.rootCauseRepository = rootCauseRepository; + this.userRepository = userRepository; + this.projectUserMemberRepository = projectUserMemberRepository; + this.projectGroupMemberRepository = projectGroupMemberRepository; this.mapper = mapper; this.communicationService = communicationService; + this.userPreferenceService = userPreferenceService; + this.cacheService = cacheService; } /** @@ -70,16 +97,24 @@ public ProjectDTO create(ProjectDTO dtoToCreate) throws NotUniqueException { validateBusinessRules(dtoToCreate); final Project entity = mapper.map(dtoToCreate, Project.class); communicationService.initializeProject(entity); - final ProjectDTO createdProject = mapper.map(repository.save(entity), ProjectDTO.class); + Project createdProject = repository.save(entity); + final ProjectDTO createdProjectDTO = mapper.map(createdProject, ProjectDTO.class); - final long projectId = createdProject.getId().longValue(); + final long projectId = createdProjectDTO.getId().longValue(); rootCauseRepository.saveAll(Arrays.asList( new RootCause(projectId, "Fragile test"), new RootCause(projectId, "Network issue"), new RootCause(projectId, "Regression"), new RootCause(projectId, "Test to update"))); - return createdProject; + String userName = SecurityContextHolder.getContext().getAuthentication().getName(); + ProjectUserMember projectUserMember = new ProjectUserMember(entity, userRepository.findByMemberName(userName)); + projectUserMember.setRole(MemberRole.ADMIN); + projectUserMemberRepository.save(projectUserMember); + updateDefaultProject(dtoToCreate); + cacheService.evictCaches(createdProject, userName); + + return createdProjectDTO; } /** @@ -90,18 +125,23 @@ public ProjectDTO create(ProjectDTO dtoToCreate) throws NotUniqueException { * @throws NotFoundException when the given entity ID is not present in database * @throws NotUniqueException when the given code or name is already used by another entity */ - public ProjectDTO update(ProjectDTO dtoToUpdate) throws BadRequestException { + public ProjectDTO update(String projectCode, ProjectDTO dtoToUpdate) throws BadRequestException { // Must update an existing entity - Optional dataBaseEntity = repository.findById(dtoToUpdate.getId()); - if (!dataBaseEntity.isPresent()) { + Project dataBaseEntity = repository.findOneByCode(projectCode); + if (dataBaseEntity == null) { throw new NotFoundException(Messages.NOT_FOUND_PROJECT, Entities.PROJECT); } + dtoToUpdate.setId(dataBaseEntity.getId()); validateBusinessRules(dtoToUpdate); final Project entity = mapper.map(dtoToUpdate, Project.class); - entity.setCommunications(dataBaseEntity.get().getCommunications()); - return mapper.map(repository.save(entity), ProjectDTO.class); + entity.setCommunications(dataBaseEntity.getCommunications()); + ProjectDTO updatedProject = mapper.map(repository.save(entity), ProjectDTO.class); + if (updateDefaultProject(dtoToUpdate)) { + cacheService.evictsUserProjectsCache(SecurityContextHolder.getContext().getAuthentication().getName()); + } + return updatedProject; } /** @@ -114,6 +154,28 @@ public List findAll() { return mapper.mapCollection(repository.findAllByOrderByName(), ProjectDTO.class); } + /** + * Get all the entities. + * + * @return the list of entities + */ + @Transactional(readOnly = true) + @Cacheable(value = "security.user.projects", key = "#userId") + public List findAll(String userId) { + Set projects = new TreeSet<>(Comparator.comparing(Project::getName)); + projects.addAll(projectUserMemberRepository.findAllProjectByUserName(userId)); + projects.addAll(projectGroupMemberRepository.findAllProjectByUserName(userId)); + String defaultProjectCode = userPreferenceService.getValue(UserPreferenceService.DEFAULT_PROJECT); + List projectDTOList = mapper.mapCollection(projects, ProjectDTO.class); + if (defaultProjectCode != null) { + Optional findFirst = projectDTOList.stream().filter(project -> defaultProjectCode.equals(project.getCode())).findFirst(); + if (findFirst.isPresent()) { + findFirst.get().setDefaultAtStartup(true); + } + } + return projectDTOList; + } + /** * Get one project by code. * @@ -144,7 +206,6 @@ public long toId(String code) throws NotFoundException { private void validateBusinessRules(ProjectDTO dto) throws NotUniqueException { validateUniqueCode(dto); validateUniqueName(dto); - switchProjectAsDefault(dto); } private void validateUniqueCode(ProjectDTO dto) throws NotUniqueException { @@ -161,13 +222,26 @@ private void validateUniqueName(ProjectDTO dto) throws NotUniqueException { } } - private void switchProjectAsDefault(ProjectDTO dto) { + private boolean updateDefaultProject(ProjectDTO dto) { if (dto.isDefaultAtStartup()) { - Project entityDataBaseWithDefaultAtStartup = repository.findByDefaultAtStartup(true); - if (entityDataBaseWithDefaultAtStartup != null && !entityDataBaseWithDefaultAtStartup.getCode().equals(dto.getCode())) { - entityDataBaseWithDefaultAtStartup.setDefaultAtStartup(false); - } + userPreferenceService.setValue(UserPreferenceService.DEFAULT_PROJECT, dto.getCode()); + return true; + } else if (dto.getCode().equals(userPreferenceService.getValue(UserPreferenceService.DEFAULT_PROJECT))) { + userPreferenceService.setValue(UserPreferenceService.DEFAULT_PROJECT, null); + return true; + } + return false; + } + + public void delete(String code) throws NotFoundException { + final Project project = repository.findOneByCode(code); + if (project == null) { + throw new NotFoundException(Messages.NOT_FOUND_PROJECT, Entities.PROJECT); } + projectGroupMemberRepository.deleteByProjectCode(code); + projectUserMemberRepository.deleteByProjectCode(code); + repository.delete(project); + cacheService.evictCaches(project); } } diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/ProjectUserMemberService.java b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectUserMemberService.java new file mode 100644 index 000000000..c1a16ca62 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/ProjectUserMemberService.java @@ -0,0 +1,43 @@ +package com.decathlon.ara.service; + +import org.springframework.stereotype.Service; + +import com.decathlon.ara.cache.CacheService; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.repository.ProjectRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; +import com.decathlon.ara.repository.UserRepository; + +@Service +public class ProjectUserMemberService extends ProjectMemberService { + + private CacheService cacheService; + + protected ProjectUserMemberService(ProjectRepository projectRepository, ProjectUserMemberRepository projectUserMemberRepository, UserRepository userRepository, CacheService cacheService) { + super(projectRepository, projectUserMemberRepository, userRepository); + this.cacheService = cacheService; + } + + @Override + protected ProjectUserMember constructMember(Project project, User member) { + return new ProjectUserMember(project, member); + } + + @Override + protected void afterAddMember(Project project, User member) { + cacheService.evictCaches(project, member.getMemberName()); + } + + @Override + protected void afterUpdateRole(Project project, User member) { + cacheService.evictsUserProjectRolesCache(project, member.getMemberName()); + } + + @Override + protected void afterDeleteMember(Project project, User member) { + cacheService.evictCaches(project, member.getMemberName()); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/UserPreferenceService.java b/code/api/api/src/main/java/com/decathlon/ara/service/UserPreferenceService.java new file mode 100644 index 000000000..16b439c69 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/UserPreferenceService.java @@ -0,0 +1,55 @@ +package com.decathlon.ara.service; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import com.decathlon.ara.domain.UserPreference; +import com.decathlon.ara.repository.UserPreferenceRepository; +import com.decathlon.ara.repository.UserRepository; + +@Service +public class UserPreferenceService { + + record Preference(String key, Object defaultValue) { + } + + public static final Preference DEFAULT_PROJECT = new Preference("defaultProject", null); + + private UserPreferenceRepository userPreferenceRepository; + private UserRepository userRepository; + + public UserPreferenceService(UserPreferenceRepository userPreferenceRepository, UserRepository userRepository) { + this.userPreferenceRepository = userPreferenceRepository; + this.userRepository = userRepository; + } + + public boolean isEnabled(Preference preference) { + return Boolean.parseBoolean(getValue(preference)); + } + + public String getValue(Preference preference) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserPreference userPreference = userPreferenceRepository.findByIdUserIdAndIdKey(authentication.getName(), preference.key()); + if (userPreference != null) { + return userPreference.getValue(); + } else { + if (preference.defaultValue() != null) { + return preference.defaultValue().toString(); + } else { + return null; + } + } + } + + public void setValue(Preference preference, Object value) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + UserPreference userPreference = userPreferenceRepository.findByIdUserIdAndIdKey(authentication.getName(), preference.key()); + if (userPreference == null) { + userPreference = new UserPreference(userRepository.findByMemberName(authentication.getName()), preference.key()); + } + userPreference.setValue(value == null ? null : value.toString()); + userPreferenceRepository.save(userPreference); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/UserService.java b/code/api/api/src/main/java/com/decathlon/ara/service/UserService.java new file mode 100644 index 000000000..f0f26e413 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/UserService.java @@ -0,0 +1,33 @@ +package com.decathlon.ara.service; + +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.decathlon.ara.Messages; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.dto.user.UserDTO; +import com.decathlon.ara.service.exception.NotFoundException; + +@Service +public class UserService { + + private UserRepository userRepository; + + public UserService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + public List findAll() { + return userRepository.findAll().stream().map(user -> new UserDTO(user.getMemberName())).toList(); + } + + public UserDTO findOne(String name) throws NotFoundException { + User user = userRepository.findByMemberName(name); + if (user == null) { + throw new NotFoundException(String.format(Messages.NOT_FOUND, "user"), "user"); + } + return new UserDTO(name); + } +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/auditing/AuditingService.java b/code/api/api/src/main/java/com/decathlon/ara/service/auditing/AuditingService.java new file mode 100644 index 000000000..d8d697c5c --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/auditing/AuditingService.java @@ -0,0 +1,60 @@ +package com.decathlon.ara.service.auditing; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.UserRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.repository.UserRoleRepository; +import com.decathlon.ara.service.dto.auditing.ProjectRoleDetails; +import com.decathlon.ara.service.dto.auditing.UserRoleDetails; + +@Service +public class AuditingService { + + private UserRepository userRepository; + private UserRoleRepository userRoleRepository; + private GroupMemberRepository groupMemberRepository; + private ProjectUserMemberRepository projectUserMemberRepository; + private ProjectGroupMemberRepository projectGroupMemberRepository; + + public AuditingService(UserRepository userRepository, UserRoleRepository userRoleRepository, GroupMemberRepository groupMemberRepository, ProjectUserMemberRepository projectUserMemberRepository, ProjectGroupMemberRepository projectGroupMemberRepository) { + this.userRepository = userRepository; + this.userRoleRepository = userRoleRepository; + this.groupMemberRepository = groupMemberRepository; + this.projectUserMemberRepository = projectUserMemberRepository; + this.projectGroupMemberRepository = projectGroupMemberRepository; + } + + public List auditUsersRoles() { + List result = new ArrayList<>(); + for (User user : userRepository.findAll()) { + UserRoleDetails details = new UserRoleDetails(user.getMemberName()); + for (UserRole userRole : userRoleRepository.findAllByIdUserId(user.getMemberName())) { + details.addRoles(userRole.getRole()); + } + for (GroupMember groupMember : groupMemberRepository.findAllByIdUserName(user.getMemberName())) { + for (ProjectGroupMember projectGroupMember : projectGroupMemberRepository.findAllByIdMemberName(groupMember.getGroupName())) { + ProjectRoleDetails projectDetails = details.getProject(projectGroupMember.getProject().getCode()); + projectDetails.addRole(projectGroupMember.getRole(), groupMember.getGroupName()); + } + } + for (ProjectUserMember projectUserMember : projectUserMemberRepository.findAllByIdMemberName(user.getMemberName())) { + ProjectRoleDetails projectDetails = details.getProject(projectUserMember.getProject().getCode()); + projectDetails.addRole(projectUserMember.getRole(), null); + } + result.add(details); + } + return result; + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/MemberRoleDetails.java b/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/MemberRoleDetails.java new file mode 100644 index 000000000..4a3db94ce --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/MemberRoleDetails.java @@ -0,0 +1,23 @@ +package com.decathlon.ara.service.dto.auditing; + +import com.decathlon.ara.domain.enumeration.MemberRole; + +public class MemberRoleDetails { + + private MemberRole memberRole; + private String inheritFromGroup; + + public MemberRoleDetails(MemberRole memberRole, String inheritFromGroup) { + this.memberRole = memberRole; + this.inheritFromGroup = inheritFromGroup; + } + + public MemberRole getMemberRole() { + return memberRole; + } + + public String getInheritFromGroup() { + return inheritFromGroup; + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/ProjectRoleDetails.java b/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/ProjectRoleDetails.java new file mode 100644 index 000000000..d29aa9e55 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/ProjectRoleDetails.java @@ -0,0 +1,30 @@ +package com.decathlon.ara.service.dto.auditing; + +import java.util.ArrayList; +import java.util.List; + +import com.decathlon.ara.domain.enumeration.MemberRole; + +public class ProjectRoleDetails { + + private String code; + private List roles; + + public ProjectRoleDetails(String code) { + this.code = code; + this.roles = new ArrayList<>(); + } + + public void addRole(MemberRole role, String inheritedFrom) { + roles.add(new MemberRoleDetails(role, inheritedFrom)); + } + + public String getCode() { + return code; + } + + public List getRoles() { + return roles; + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/UserRoleDetails.java b/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/UserRoleDetails.java new file mode 100644 index 000000000..c1135b86b --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/dto/auditing/UserRoleDetails.java @@ -0,0 +1,47 @@ +package com.decathlon.ara.service.dto.auditing; + +import java.util.ArrayList; +import java.util.List; + +import com.decathlon.ara.domain.enumeration.UserSecurityRole; + +public class UserRoleDetails { + + private String userName; + private List roles; + private List projects; + + public UserRoleDetails(String userName) { + this.userName = userName; + roles = new ArrayList<>(); + projects = new ArrayList<>(); + } + + public ProjectRoleDetails getProject(String code) { + for (ProjectRoleDetails project : projects) { + if (code.equals(project.getCode())){ + return project; + } + } + ProjectRoleDetails newProject = new ProjectRoleDetails(code); + projects.add(newProject); + return newProject; + } + + public void addRoles(UserSecurityRole role) { + roles.add(role); + } + + public String getUserName() { + return userName; + } + + public List getRoles() { + return roles; + } + + public List getProjects() { + return projects; + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/dto/group/GroupDTO.java b/code/api/api/src/main/java/com/decathlon/ara/service/dto/group/GroupDTO.java new file mode 100644 index 000000000..399e29fb5 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/dto/group/GroupDTO.java @@ -0,0 +1,23 @@ +package com.decathlon.ara.service.dto.group; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +public class GroupDTO { + + @NotNull(message = "name is mandatory") + @Pattern(regexp = "^[A-Za-z0-9_-]+$", message = "Only alphanumeric characters, dash and underscore are allowed in name") + private String name; + + public GroupDTO() { + } + + public GroupDTO(String name) { + this.name = name; + } + + public String getName() { + return name; + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/dto/member/MemberDTO.java b/code/api/api/src/main/java/com/decathlon/ara/service/dto/member/MemberDTO.java new file mode 100644 index 000000000..76ced8ec7 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/dto/member/MemberDTO.java @@ -0,0 +1,30 @@ +package com.decathlon.ara.service.dto.member; + +import javax.validation.constraints.NotNull; + +import com.decathlon.ara.domain.enumeration.MemberRole; + +public class MemberDTO { + + @NotNull(message = "name is mandatory") + private String name; + @NotNull(message = "role is mandatory") + private MemberRole role; + + public MemberDTO() { + } + + public MemberDTO(String name, MemberRole role) { + this.name = name; + this.role = role; + } + + public String getName() { + return name; + } + + public MemberRole getRole() { + return role; + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/dto/project/ProjectDTO.java b/code/api/api/src/main/java/com/decathlon/ara/service/dto/project/ProjectDTO.java index 9e28da0e4..1a2e23a6e 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/service/dto/project/ProjectDTO.java +++ b/code/api/api/src/main/java/com/decathlon/ara/service/dto/project/ProjectDTO.java @@ -68,6 +68,10 @@ public String getCode() { return code; } + public void setCode(String code) { + this.code = code; + } + public String getName() { return name; } @@ -76,4 +80,8 @@ public boolean isDefaultAtStartup() { return defaultAtStartup; } + public void setDefaultAtStartup(boolean defaultAtStartup) { + this.defaultAtStartup = defaultAtStartup; + } + } diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/dto/user/UserDTO.java b/code/api/api/src/main/java/com/decathlon/ara/service/dto/user/UserDTO.java new file mode 100644 index 000000000..fae14a03f --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/dto/user/UserDTO.java @@ -0,0 +1,26 @@ +package com.decathlon.ara.service.dto.user; + +import java.util.List; + +import com.decathlon.ara.domain.enumeration.UserSecurityRole; + +public class UserDTO { + + private String name; + private List roles; + + public UserDTO() { + } + + public UserDTO(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public List getRoles() { + return roles; + } +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/service/security/SecurityService.java b/code/api/api/src/main/java/com/decathlon/ara/service/security/SecurityService.java new file mode 100644 index 000000000..117c0ca7f --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/service/security/SecurityService.java @@ -0,0 +1,107 @@ +package com.decathlon.ara.service.security; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.UserRole; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.domain.enumeration.UserSecurityRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.repository.UserRoleRepository; + +@Service +@Transactional(readOnly = true) +public class SecurityService { + + private ProjectUserMemberRepository projectUserMemberRepository; + private ProjectGroupMemberRepository projectGroupMemberRepository; + private UserRepository userRepository; + private UserRoleRepository userRoleRepository; + private GroupMemberRepository groupMemberRepository; + + private volatile boolean adminCreated; + + @Value("${ara.security.admin.init.name:}") + private String firstAdminName; + + @Value("${ara.security.newUser.role:PROJECT_OR_GROUP_CREATOR}") + private UserSecurityRole newUserRole; + + public SecurityService(ProjectUserMemberRepository projectUserMemberRepository, ProjectGroupMemberRepository projectGroupMemberRepository, UserRepository userRepository, UserRoleRepository userRoleRepository, GroupMemberRepository groupMemberRepository) { + this.projectUserMemberRepository = projectUserMemberRepository; + this.projectGroupMemberRepository = projectGroupMemberRepository; + this.userRepository = userRepository; + this.userRoleRepository = userRoleRepository; + this.groupMemberRepository = groupMemberRepository; + adminCreated = userRoleRepository.existsByIdRole(UserSecurityRole.ADMIN); + } + + @Cacheable(value = "security.user.project.roles", key = "#projectCode.concat(#userName)") + public Set getProjectMemberRoles(String projectCode, String userName) { + Set memberRoles = new HashSet<>(); + ProjectUserMember userMember = projectUserMemberRepository.findByProjectCodeAndIdMemberName(projectCode, userName); + if (userMember != null) { + memberRoles.add(userMember.getRole()); + } + for (ProjectGroupMember groupMember : projectGroupMemberRepository.findAllProjectGroupMemberByProjectCodeAndUserName(projectCode, userName)) { + memberRoles.add(groupMember.getRole()); + } + return memberRoles; + } + + public Set getGroupMemberRoles(String groupName, String userName) { + Set memberRoles = new HashSet<>(); + GroupMember groupMember = groupMemberRepository.findByContainerIdentifierAndMemberName(groupName, userName); + if (groupMember != null) { + memberRoles.add(groupMember.getRole()); + } + return memberRoles; + } + + @Transactional + public User initUser(String userName, String issuer) { + User user = userRepository.findByNameAndIssuer(userName, issuer); + if (user == null) { + user = new User(userName, issuer); + userRepository.save(user); + if (!adminCreated && (firstAdminName == null || firstAdminName.isBlank() || firstAdminName.equals(userName))) { + synchronized (SecurityService.class) { + if (!adminCreated) { + initUserRole(user, UserSecurityRole.ADMIN); + adminCreated = true; + } + } + } + initUserRole(user, newUserRole); + } + return user; + } + + private void initUserRole(User user, UserSecurityRole role) { + if (role != null) { + UserRole userRole = userRoleRepository.findByIdUserIdAndIdRole(user.getId(), role); + if (userRole == null) { + userRole = new UserRole(user, role); + userRoleRepository.save(userRole); + } + } + } + + public List getUserRoles(String userId) { + return userRoleRepository.findAllByIdUserId(userId).stream().map(UserRole::getRole).toList(); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/FunctionalityResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/FunctionalityResource.java index 9f276b12d..9ca7526b0 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/web/rest/FunctionalityResource.java +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/FunctionalityResource.java @@ -246,7 +246,7 @@ public ResponseEntity getCoverage(@PathVariable String projectCode) * @param projectCode the current project code * @return the JSON of all the exporters available. */ - @RequestMapping(method = RequestMethod.OPTIONS, path = "/export") + @RequestMapping(method = RequestMethod.OPTIONS, value = "/export") public ResponseEntity> getExportOptions(@PathVariable String projectCode) { return ResponseEntity.ok().body(service.listAvailableExporters()); } diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupMemberResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupMemberResource.java new file mode 100644 index 000000000..7787216b0 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupMemberResource.java @@ -0,0 +1,29 @@ +package com.decathlon.ara.web.rest; + +import java.util.Map; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.service.GroupMemberService; +import com.decathlon.ara.web.rest.util.RestConstants; + +@RestController +@RequestMapping(GroupMemberResource.PATH) +public class GroupMemberResource extends MemberResource { + + static final String PATH = RestConstants.API_PATH + "/groups/{groupName}" + MemberResource.BASE_PATH; + + public GroupMemberResource(GroupMemberService groupMemberService) { + super(groupMemberService); + } + + @Override + protected String getIdentifier(Map pathVariables) { + return pathVariables.get("groupName"); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupResource.java new file mode 100644 index 000000000..2ed14a4bb --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/GroupResource.java @@ -0,0 +1,56 @@ +package com.decathlon.ara.web.rest; + +import java.util.List; + +import javax.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.decathlon.ara.service.GroupService; +import com.decathlon.ara.service.dto.group.GroupDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; +import com.decathlon.ara.service.exception.NotUniqueException; +import com.decathlon.ara.web.rest.util.HeaderUtil; +import com.decathlon.ara.web.rest.util.RestConstants; + +@RestController +@RequestMapping(GroupResource.PATH) +public class GroupResource { + + static final String PATH = RestConstants.API_PATH + "/groups"; + + private GroupService groupService; + + public GroupResource(GroupService groupService) { + this.groupService = groupService; + } + + @GetMapping + public List getAll() { + return groupService.findAll(); + } + + @GetMapping("/{groupName}") + public GroupDTO get(@PathVariable String groupName) throws NotFoundException { + return groupService.findOne(groupName); + } + + @PostMapping + public ResponseEntity create(@Valid @RequestBody GroupDTO groupDto) throws NotUniqueException { + GroupDTO group = groupService.create(groupDto); + return ResponseEntity.created(HeaderUtil.uri(PATH + "/" + group.getName())).body(group); + } + + @DeleteMapping("/{groupName}") + public void delete(@PathVariable String groupName) throws BadRequestException { + groupService.delete(groupName); + } +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/MemberResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/MemberResource.java new file mode 100644 index 000000000..7a94e599c --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/MemberResource.java @@ -0,0 +1,62 @@ +package com.decathlon.ara.web.rest; + +import java.util.List; +import java.util.Map; + +import javax.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.MemberRelationship; +import com.decathlon.ara.service.MemberService; +import com.decathlon.ara.service.dto.member.MemberDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; + +public abstract class MemberResource> { + + protected static final String BASE_PATH = "/members"; + + private MemberService memberService; + + protected MemberResource(MemberService memberService) { + this.memberService = memberService; + } + + protected abstract String getIdentifier(Map pathVariables); + + @GetMapping + public List getAll(@PathVariable Map pathVariables) { + return memberService.findAll(getIdentifier(pathVariables)); + } + + @GetMapping("/{memberName}") + public MemberDTO get(@PathVariable Map pathVariables, @PathVariable String memberName) throws NotFoundException { + return memberService.findOne(getIdentifier(pathVariables), memberName); + } + + @PostMapping + public ResponseEntity addMember(@PathVariable Map pathVariables, @Valid @RequestBody MemberDTO memberDto) throws BadRequestException { + return ResponseEntity.created(ServletUriComponentsBuilder.fromCurrentRequestUri().scheme(null).host(null).port(null).path("/" + memberDto.getName()).build().toUri()).body(memberService.addMember(getIdentifier(pathVariables), memberDto)); + } + + @PatchMapping("/{memberName}") + public MemberDTO updateMemberRole(@PathVariable Map pathVariables, @PathVariable String memberName, @Valid @RequestBody MemberDTO memberDto) throws BadRequestException { + return memberService.updateMemberRole(getIdentifier(pathVariables), memberName, memberDto.getRole()); + } + + @DeleteMapping("/{memberName}") + public void deleteMember(@PathVariable Map pathVariables, @PathVariable String memberName) throws BadRequestException { + memberService.deleteMember(getIdentifier(pathVariables), memberName); + + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectAdministrationResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectAdministrationResource.java new file mode 100644 index 000000000..8a6bf51f0 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectAdministrationResource.java @@ -0,0 +1,32 @@ +package com.decathlon.ara.web.rest; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.decathlon.ara.service.ProjectService; +import com.decathlon.ara.service.dto.project.ProjectDTO; + +@RestController +@RequestMapping("/api/admin/projects") +public class ProjectAdministrationResource { + + private ProjectService projectService; + + public ProjectAdministrationResource(ProjectService projectService) { + this.projectService = projectService; + } + + /** + * GET all entities. + * + * @return the ResponseEntity with status 200 (OK) and the list of entities in body + */ + @GetMapping + public List getAll() { + return projectService.findAll(); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectGroupMemberResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectGroupMemberResource.java new file mode 100644 index 000000000..e945dc6ec --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectGroupMemberResource.java @@ -0,0 +1,20 @@ +package com.decathlon.ara.web.rest; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.service.ProjectGroupMemberService; + +@RestController +@RequestMapping(ProjectGroupMemberResource.PATH) +public class ProjectGroupMemberResource extends ProjectMemberResource { + + static final String PATH = ProjectMemberResource.BASE_PATH + "/groups"; + + public ProjectGroupMemberResource(ProjectGroupMemberService projectMemberService) { + super(projectMemberService); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectMemberResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectMemberResource.java new file mode 100644 index 000000000..3c80a2de9 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectMemberResource.java @@ -0,0 +1,24 @@ +package com.decathlon.ara.web.rest; + +import java.util.Map; + +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.MemberRelationship; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.service.MemberService; +import com.decathlon.ara.web.rest.util.RestConstants; + +public abstract class ProjectMemberResource> extends MemberResource { + + static final String BASE_PATH = RestConstants.PROJECT_API_PATH + MemberResource.BASE_PATH; + + protected ProjectMemberResource(MemberService memberService) { + super(memberService); + } + + @Override + protected String getIdentifier(Map pathVariables) { + return pathVariables.get("projectCode"); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectResource.java index 033186e5f..807f1be4d 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectResource.java +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectResource.java @@ -24,6 +24,7 @@ import javax.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -39,6 +40,7 @@ import com.decathlon.ara.service.exception.NotUniqueException; import com.decathlon.ara.web.rest.util.HeaderUtil; import com.decathlon.ara.web.rest.util.ResponseUtil; +import com.decathlon.ara.web.rest.util.RestConstants; /** * REST controller for managing Projects. @@ -88,11 +90,10 @@ public ResponseEntity create(@Valid @RequestBody ProjectDTO dtoToCre * @return the ResponseEntity with status 200 (OK) and with body the updated entity, or with status 400 (Bad Request) if the entity is not * valid, or with status 500 (Internal Server Error) if the entity couldn't be updated */ - @PutMapping("/{id:[0-9]+}") - public ResponseEntity update(@PathVariable Long id, @Valid @RequestBody ProjectDTO dtoToUpdate) { - dtoToUpdate.setId(id); // HTTP PUT requires the URL to be the URL of the entity + @PutMapping("/" + RestConstants.PROJECT_CODE_REQUEST_PARAMETER) + public ResponseEntity update(@PathVariable String projectCode, @Valid @RequestBody ProjectDTO dtoToUpdate) { try { - ProjectDTO updatedDto = service.update(dtoToUpdate); + ProjectDTO updatedDto = service.update(projectCode, dtoToUpdate); return ResponseEntity.ok() .headers(HeaderUtil.entityUpdated(NAME, updatedDto.getId())) .body(updatedDto); @@ -107,8 +108,8 @@ public ResponseEntity update(@PathVariable Long id, @Valid @RequestB * @return the ResponseEntity with status 200 (OK) and the list of entities in body */ @GetMapping("") - public List getAll() { - return service.findAll(); + public List getAll(Authentication authentication) { + return service.findAll(authentication.getName()); } } diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectUserMemberResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectUserMemberResource.java new file mode 100644 index 000000000..caadb16c9 --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/ProjectUserMemberResource.java @@ -0,0 +1,20 @@ +package com.decathlon.ara.web.rest; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.service.ProjectUserMemberService; + +@RestController +@RequestMapping(ProjectUserMemberResource.PATH) +public class ProjectUserMemberResource extends ProjectMemberResource { + + static final String PATH = ProjectMemberResource.BASE_PATH + "/users"; + + public ProjectUserMemberResource(ProjectUserMemberService projectMemberService) { + super(projectMemberService); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/TemplateResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/TemplateResource.java index 49eb3a0a2..1ccf41217 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/web/rest/TemplateResource.java +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/TemplateResource.java @@ -32,7 +32,7 @@ public TemplateResource(ExecutionHistoryService executionHistoryService, Project this.projectService = projectService; } - @GetMapping("cycle-execution") + @GetMapping("/cycle-execution") public String nrtCycle(@RequestParam("project") String projectCode, @RequestParam String branch, @RequestParam String cycle, diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/audits/AuditingResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/audits/AuditingResource.java new file mode 100644 index 000000000..841fec73b --- /dev/null +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/audits/AuditingResource.java @@ -0,0 +1,30 @@ +package com.decathlon.ara.web.rest.audits; + +import java.util.List; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.decathlon.ara.service.auditing.AuditingService; +import com.decathlon.ara.service.dto.auditing.UserRoleDetails; +import com.decathlon.ara.web.rest.util.RestConstants; + +@RestController +@RequestMapping(AuditingResource.PATH) +public class AuditingResource { + + static final String PATH = RestConstants.API_PATH + "/auditing"; + + private AuditingService auditingService; + + public AuditingResource(AuditingService auditsService) { + this.auditingService = auditsService; + } + + @GetMapping("/users-roles") + public List auditUserRole(){ + return auditingService.auditUsersRoles(); + } + +} diff --git a/code/api/api/src/main/java/com/decathlon/ara/web/rest/authentication/UserResource.java b/code/api/api/src/main/java/com/decathlon/ara/web/rest/authentication/UserResource.java index c73311c84..57c6e0bf1 100644 --- a/code/api/api/src/main/java/com/decathlon/ara/web/rest/authentication/UserResource.java +++ b/code/api/api/src/main/java/com/decathlon/ara/web/rest/authentication/UserResource.java @@ -2,28 +2,47 @@ import static com.decathlon.ara.web.rest.util.RestConstants.API_PATH; +import java.util.List; import java.util.Optional; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import com.decathlon.ara.service.UserService; import com.decathlon.ara.service.dto.authentication.AuthenticationUserDetailsDTO; +import com.decathlon.ara.service.dto.user.UserDTO; +import com.decathlon.ara.service.exception.NotFoundException; @RestController @RequestMapping(UserResource.PATH) public class UserResource { - static final String PATH = API_PATH + "/user"; + static final String PATH = API_PATH + "/users"; - @GetMapping("/details") - public AuthenticationUserDetailsDTO getUserDetails( - @AuthenticationPrincipal OidcUser oidcUser, - @AuthenticationPrincipal OAuth2User oauth2User) { - if (oidcUser != null) { + private UserService userService; + + public UserResource(UserService userService) { + this.userService = userService; + } + + @GetMapping + public List getAll() { + return userService.findAll(); + } + + @GetMapping("/{userName}") + public UserDTO getUserDetails(@PathVariable String userName) throws NotFoundException { + return userService.findOne(userName); + } + + @GetMapping("/current/details") + public AuthenticationUserDetailsDTO getUserDetails(@AuthenticationPrincipal OAuth2User oauth2User) { + if (oauth2User instanceof OidcUser oidcUser) { return new AuthenticationUserDetailsDTO( oidcUser.getSubject(), oidcUser.getFullName(), @@ -32,15 +51,12 @@ public AuthenticationUserDetailsDTO getUserDetails( oidcUser.getPicture()); } - if (oauth2User != null) { - return new AuthenticationUserDetailsDTO( - safeStringAttribute(oauth2User, "id"), - safeStringAttribute(oauth2User, "name"), - safeStringAttribute(oauth2User, "login"), - safeStringAttribute(oauth2User, "email"), - safeStringAttribute(oauth2User, "avatar_url")); - } - return new AuthenticationUserDetailsDTO(); + return new AuthenticationUserDetailsDTO( + safeStringAttribute(oauth2User, "id"), + safeStringAttribute(oauth2User, "name"), + safeStringAttribute(oauth2User, "login"), + safeStringAttribute(oauth2User, "email"), + safeStringAttribute(oauth2User, "avatar_url")); } private String safeStringAttribute(OAuth2User oauth2User, String attribute) { diff --git a/code/api/api/src/test/java/com/decathlon/ara/authentication/UserResourceTest.java b/code/api/api/src/test/java/com/decathlon/ara/authentication/UserResourceTest.java deleted file mode 100644 index ab7c172c8..000000000 --- a/code/api/api/src/test/java/com/decathlon/ara/authentication/UserResourceTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.decathlon.ara.authentication; - -import static org.mockito.Mockito.when; - -import com.decathlon.ara.web.rest.authentication.UserResource; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.core.user.OAuth2User; - -@ExtendWith(MockitoExtension.class) -class UserResourceTest { - - @Mock - private OidcUser oidcUser; - - @Mock - private OAuth2User oauth2User; - - private UserResource userResource = new UserResource(); - - @Test - void whenOIDCUser_userDetails_should_not_be_null() { - when(oidcUser.getSubject()).thenReturn("oidc_id"); - var userDetails = userResource.getUserDetails(oidcUser, null); - Assertions.assertNotNull(userDetails); - Assertions.assertEquals("oidc_id", userDetails.getId()); - } - - @Test - void whenOAauth2User_userDetails_should_not_be_null() { - when(oauth2User.getAttribute("id")).thenReturn("oauth2_id"); - var userDetails = userResource.getUserDetails(null, oauth2User); - Assertions.assertNotNull(userDetails); - Assertions.assertEquals("oauth2_id", userDetails.getId()); - } - - @Test - @MockitoSettings(strictness = Strictness.LENIENT) - void whenOAauth2UserAndOIDCUser_userDetails_should_be_generated_from_oidc() { - when(oauth2User.getAttribute("name")).thenReturn("oauth2_name"); - when(oidcUser.getFullName()).thenReturn("oidc_name"); - var userDetails = userResource.getUserDetails(oidcUser, oauth2User); - Assertions.assertNotNull(userDetails); - Assertions.assertEquals("oidc_name", userDetails.getName()); - } - - @Test - void whenNoUser_userDetails_should_not_be_null() { - Assertions.assertNotNull(userResource.getUserDetails(null, null)); - } - -} diff --git a/code/api/api/src/test/java/com/decathlon/ara/cache/CacheServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/cache/CacheServiceTest.java new file mode 100644 index 000000000..3279affc8 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/cache/CacheServiceTest.java @@ -0,0 +1,227 @@ +package com.decathlon.ara.cache; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.ehcache.EhCacheCache; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; + +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.repository.GroupMemberRepository; + +import net.sf.ehcache.Ehcache; + +@ExtendWith(MockitoExtension.class) +class CacheServiceTest { + + @Mock + private CacheManager cacheManager; + @Mock + private GroupMemberRepository groupMemberRepository; + + @InjectMocks + private CacheService cacheService; + + private static final String USER_PROJECTS_CACHE_NAME = "security.user.projects"; + private static final String USER_PROJECT_ROLES_CACHE_NAME = "security.user.project.roles"; + + @Test + void evictCachesForProjectShouldClearUserProjectsCacheWhenCacheExists() { + Project project = new Project("test", "test"); + Cache cache = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(null); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(cache); + + cacheService.evictCaches(project); + + Mockito.verify(cache).clear(); + } + + @Test + void evictCachesForProjectShouldClearUserProjectRolesCacheWhenCacheExistsAndItsNotAnEhCache() { + Project project = new Project("test", "test"); + Cache cache = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(null); + + cacheService.evictCaches(project); + + Mockito.verify(cache).clear(); + } + + @Test + void evictCachesForProjectShouldEvictMatchingKeysInUserProjectRolesCacheWhenCacheExistsAndItsAnEhCache() { + Project project = new Project("test", "test"); + EhCacheCache cache = Mockito.mock(EhCacheCache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(null); + Ehcache nativeCache = Mockito.mock(Ehcache.class); + Mockito.when(cache.getNativeCache()).thenReturn(nativeCache); + Mockito.when(nativeCache.getKeys()).thenReturn(List.of("testuser1", "testuser2", "totouser1", "totouser2", "testuser3")); + + cacheService.evictCaches(project); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + Mockito.verify(cache, Mockito.times(3)).evict(captor.capture()); + List evictedKeys = captor.getAllValues(); + Assertions.assertEquals(3, evictedKeys.size()); + Assertions.assertEquals("testuser1", evictedKeys.get(0)); + Assertions.assertEquals("testuser2", evictedKeys.get(1)); + Assertions.assertEquals("testuser3", evictedKeys.get(2)); + } + + @Test + void evictCachesForProjectShouldDoNothingUntilCommitOfTransactionIfTransactionStartedBeforeCall() { + try { + TransactionSynchronizationManager.initSynchronization(); + Project project = new Project("test", "test"); + Cache cache = Mockito.mock(Cache.class); + Cache cache2 = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(cache2); + + cacheService.evictCaches(project); + + Mockito.verifyNoInteractions(cache); + Mockito.verifyNoInteractions(cache2); + + TransactionSynchronizationUtils.triggerAfterCommit(); + + Mockito.verify(cache).clear(); + Mockito.verify(cache2).clear(); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + + @Test + void evictCachesForProjectAndUserNameShouldEvictMatchingKeyInUserProjectsCacheWhenCacheExists() { + Project project = new Project("test", "test"); + User user = new User("name", "issuer"); + Cache cache = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(null); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(cache); + + cacheService.evictCaches(project, user.getMemberName()); + + Mockito.verify(cache).evict(user.getMemberName()); + } + + @Test + void evictCachesForProjectAndUserNameShouldEvictMatchingKeyInUserProjectRolesCacheWhenCacheExists() { + Project project = new Project("test", "test"); + User user = new User("name", "issuer"); + Cache cache = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(null); + + cacheService.evictCaches(project, user.getMemberName()); + + Mockito.verify(cache).evict(project.getCode().concat(user.getMemberName())); + } + + @Test + void evictCachesForProjectAndUserNameShouldDoNothingUntilCommitOfTransactionIfTransactionStartedBeforeCall() { + try { + TransactionSynchronizationManager.initSynchronization(); + Project project = new Project("test", "test"); + User user = new User("name", "issuer"); + Cache cache = Mockito.mock(Cache.class); + Cache cache2 = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(cache2); + + cacheService.evictCaches(project, user.getMemberName()); + + Mockito.verifyNoInteractions(cache); + Mockito.verifyNoInteractions(cache2); + + TransactionSynchronizationUtils.triggerAfterCommit(); + + Mockito.verify(cache).evict(project.getCode().concat(user.getMemberName())); + Mockito.verify(cache2).evict(user.getMemberName()); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + + @Test + void evictCachesForUserNameShouldEvictMatchingKeyInUserProjectCacheWhenCacheExists() { + User user = new User("name", "issuer"); + Cache cache = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(null); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(cache); + + cacheService.evictCaches(user.getMemberName()); + + Mockito.verify(cache).evict(user.getMemberName()); + } + + @Test + void evictCachesForUserNameShouldClearUserProjectRolesCacheWhenCacheExistsAndItsNotAnEhCache() { + User user = new User("name", "issuer"); + Cache cache = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(null); + + cacheService.evictCaches(user.getMemberName()); + + Mockito.verify(cache).clear(); + } + + @Test + void evictCachesForUserNameShouldEvictMatchingKeyInUserProjectRolesCacheWhenCacheExistsAndItsAnEhCache() { + User user = new User("user1", "issuer"); + EhCacheCache cache = Mockito.mock(EhCacheCache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(null); + Ehcache nativeCache = Mockito.mock(Ehcache.class); + Mockito.when(cache.getNativeCache()).thenReturn(nativeCache); + Mockito.when(nativeCache.getKeys()).thenReturn(List.of("testuser1-issuer", "testuser2-issuer", "totouser1-issuer", "totouser2-issuer", "testuser3-issuer")); + + cacheService.evictCaches(user.getMemberName()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + Mockito.verify(cache, Mockito.times(2)).evict(captor.capture()); + List evictedKeys = captor.getAllValues(); + Assertions.assertEquals(2, evictedKeys.size()); + Assertions.assertEquals("testuser1-issuer", evictedKeys.get(0)); + Assertions.assertEquals("totouser1-issuer", evictedKeys.get(1)); + } + + @Test + void evictCachesForUserNameShouldDoNothingUntilCommitOfTransactionIfTransactionStartedBeforeCall() { + try { + TransactionSynchronizationManager.initSynchronization(); + User user = new User("name", "issuer"); + Cache cache = Mockito.mock(Cache.class); + Cache cache2 = Mockito.mock(Cache.class); + Mockito.when(cacheManager.getCache(USER_PROJECT_ROLES_CACHE_NAME)).thenReturn(cache); + Mockito.when(cacheManager.getCache(USER_PROJECTS_CACHE_NAME)).thenReturn(cache2); + + cacheService.evictCaches(user.getMemberName()); + + Mockito.verifyNoInteractions(cache); + Mockito.verifyNoInteractions(cache2); + + TransactionSynchronizationUtils.triggerAfterCommit(); + + Mockito.verify(cache).clear(); + Mockito.verify(cache2).evict(user.getMemberName()); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/cache/CacheableTest.java b/code/api/api/src/test/java/com/decathlon/ara/cache/CacheableTest.java new file mode 100644 index 000000000..bd3637a78 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/cache/CacheableTest.java @@ -0,0 +1,1006 @@ +package com.decathlon.ara.cache; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.ehcache.EhCacheCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.util.AopTestUtils; + +import com.decathlon.ara.configuration.security.TestAuthentication; +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.GroupRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.ProjectRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; +import com.decathlon.ara.repository.RootCauseRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.CommunicationService; +import com.decathlon.ara.service.GroupMemberService; +import com.decathlon.ara.service.ProjectGroupMemberService; +import com.decathlon.ara.service.ProjectService; +import com.decathlon.ara.service.ProjectUserMemberService; +import com.decathlon.ara.service.UserPreferenceService; +import com.decathlon.ara.service.dto.member.MemberDTO; +import com.decathlon.ara.service.dto.project.ProjectDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotUniqueException; +import com.decathlon.ara.service.mapper.GenericMapper; +import com.decathlon.ara.service.security.SecurityService; +import com.decathlon.ara.util.TestUtil; + +@SpringBootTest +@ContextConfiguration(classes = { CacheableTest.CacheITTestConfig.class, ProjectGroupMemberService.class, ProjectUserMemberService.class, GroupMemberService.class }) +class CacheableTest { + + private static final EhCacheCacheManager CACHE_MANAGER = new EhCacheCacheManager(); + + @Autowired + private SecurityService securityService; + + private SecurityService mockSecurityService; + + @Autowired + private ProjectService projectService; + + private ProjectService mockProjectService; + + @Autowired + private ProjectRepository projectRepository; + + @Autowired + private GenericMapper genericMapper; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserPreferenceService userPreferenceService; + + @Autowired + private ProjectUserMemberService projectUserMemberService; + + @Autowired + private ProjectGroupMemberService projectGroupMemberService; + + @Autowired + private GroupRepository groupRepository; + + @Autowired + private GroupMemberService groupMemberService; + + @Autowired + private GroupMemberRepository groupMemberRepository; + + @Autowired + private ProjectUserMemberRepository projectUserMemberRepository; + + @Autowired + private ProjectGroupMemberRepository projectGroupMemberRepository; + + private static final User USER = new User("name", "issuer"); + private static final Project PROJECT = new Project("code", "name"); + static { + TestUtil.setField(PROJECT, "id", 1L); + } + private static final Group GROUP = new Group("name"); + + @EnableCaching + @TestConfiguration + static class CacheITTestConfig { + + @Bean + public CacheManager cacheManager() { + return CACHE_MANAGER; + } + + @Bean + public ProjectService projectService() { + return Mockito.spy(new ProjectService(projectRepository(), rootCauseRepository(), userRepository(), projectUserMemberRepository(), projectGroupMemberRepository(), genericMapper(), communicationService(), userPreferenceService(), cacheService())); + } + + @Bean + public CacheService cacheService() { + return new CacheService(cacheManager()); + } + + @Bean + public SecurityService securityService() { + return Mockito.mock(SecurityService.class); + } + + @Bean + public ProjectRepository projectRepository() { + return Mockito.mock(ProjectRepository.class); + } + + @Bean + public RootCauseRepository rootCauseRepository() { + return Mockito.mock(RootCauseRepository.class); + } + + @Bean + public UserRepository userRepository() { + return Mockito.mock(UserRepository.class); + } + + @Bean + public ProjectUserMemberRepository projectUserMemberRepository() { + return Mockito.mock(ProjectUserMemberRepository.class); + } + + @Bean + public ProjectGroupMemberRepository projectGroupMemberRepository() { + return Mockito.mock(ProjectGroupMemberRepository.class); + } + + @Bean + public GenericMapper genericMapper() { + return Mockito.mock(GenericMapper.class); + } + + @Bean + public CommunicationService communicationService() { + return Mockito.mock(CommunicationService.class); + } + + @Bean + public UserPreferenceService userPreferenceService() { + return Mockito.mock(UserPreferenceService.class); + } + + @Bean + public GroupMemberRepository groupMemberRepository() { + return Mockito.mock(GroupMemberRepository.class); + } + + @Bean + public GroupRepository groupRepository() { + return Mockito.mock(GroupRepository.class); + } + + } + + @BeforeEach + void beforeEach() { + mockSecurityService = AopTestUtils.getTargetObject(securityService); + mockProjectService = AopTestUtils.getTargetObject(projectService); + + Mockito.reset(mockSecurityService, mockProjectService, projectRepository, genericMapper, userPreferenceService, projectUserMemberRepository, projectGroupMemberRepository, groupMemberRepository); + + final AtomicInteger securityServiceCallCounter = new AtomicInteger(0); + + Mockito.when(mockSecurityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName())).thenAnswer(invocationOnMock -> { + securityServiceCallCounter.incrementAndGet(); + return new HashSet<>() { + + private static final long serialVersionUID = 1L; + + @Override + public int size() { + return securityServiceCallCounter.get(); + } + }; + }); + + final AtomicInteger projectServiceCallCounter = new AtomicInteger(0); + + Mockito.when(mockProjectService.findAll(USER.getMemberName())).thenAnswer(invocationOnMock -> { + projectServiceCallCounter.incrementAndGet(); + return new ArrayList<>() { + + private static final long serialVersionUID = 1L; + + @Override + public int size() { + return projectServiceCallCounter.get(); + } + }; + }); + } + + @AfterEach + void afterEach() { + for (String cacheName : CACHE_MANAGER.getCacheNames()) { + CACHE_MANAGER.getCache(cacheName).clear(); + } + } + + @AfterAll + static void afterAll() { + //without this, AraExporterTest failed due to duplicate CacheManager (see net.sf.CacheManager#assertNoCacheManagerExistsWithSameName) + CACHE_MANAGER.getCacheManager().shutdown(); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenProjectIsCreated() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(null, PROJECT.getCode(), PROJECT.getName(), false); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.save(PROJECT)).thenReturn(PROJECT); + ProjectDTO result = new ProjectDTO(1l, PROJECT.getCode(), PROJECT.getName(), true); + Mockito.when(genericMapper.map(PROJECT, ProjectDTO.class)).thenReturn(result); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.create(projectDTO); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenProjectIsDeleted() throws BadRequestException { + Mockito.when(projectRepository.findOneByCode(PROJECT.getCode())).thenReturn(PROJECT); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.delete(PROJECT.getCode()); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenUserAddedAsMemberOfProject() throws BadRequestException { + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.addMember(PROJECT.getCode(), new MemberDTO(USER.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenUserRoleIsChangedOnProject() throws BadRequestException { + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), USER.getMemberName())).thenReturn(new ProjectUserMember(PROJECT, USER)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.updateMemberRole(PROJECT.getCode(), USER.getMemberName(), MemberRole.ADMIN); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenUserDeletedAsMemberOfProject() throws BadRequestException { + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), USER.getMemberName())).thenReturn(new ProjectUserMember(PROJECT, USER)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.deleteMember(PROJECT.getCode(), USER.getMemberName()); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenGroupContainingUserIsAddedAsMemberOfProject() throws BadRequestException { + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(groupRepository.findByMemberName(GROUP.getName())).thenReturn(GROUP); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.addMember(PROJECT.getCode(), new MemberDTO(GROUP.getName(), MemberRole.ADMIN)); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenGroupContainingUserRoleIsUpdatedOnProject() throws BadRequestException { + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(PROJECT, GROUP)); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.updateMemberRole(PROJECT.getCode(), GROUP.getName(), MemberRole.ADMIN); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenGroupContainingUserIsDeletedAsMemberOfProject() throws BadRequestException { + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(PROJECT, GROUP)); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.deleteMember(PROJECT.getCode(), GROUP.getName()); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenUserIsAddedAsMemberOfGroup() throws BadRequestException { + Mockito.when(groupRepository.findByContainerIdentifier(GROUP.getName())).thenReturn(GROUP); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + groupMemberService.addMember(GROUP.getName(), new MemberDTO(USER.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenUserRoleIsUpdatedOnGroup() throws BadRequestException { + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName(GROUP.getName(), USER.getMemberName())).thenReturn(new GroupMember(GROUP, USER)); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + groupMemberService.updateMemberRole(GROUP.getName(), USER.getMemberName(), MemberRole.ADMIN); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndClearedWhenUserIsDeletedAsMemberOfGroup() throws BadRequestException { + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName(GROUP.getName(), USER.getMemberName())).thenReturn(new GroupMember(GROUP, USER)); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + groupMemberService.deleteMember(GROUP.getName(), USER.getMemberName()); + + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(2, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenProjectIsCreatedByAnotherUser() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + ProjectDTO projectDTO = new ProjectDTO(null, PROJECT.getCode(), PROJECT.getName(), false); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.save(PROJECT)).thenReturn(PROJECT); + ProjectDTO result = new ProjectDTO(1l, PROJECT.getCode(), PROJECT.getName(), true); + Mockito.when(genericMapper.map(PROJECT, ProjectDTO.class)).thenReturn(result); + Mockito.when(userRepository.findByMemberName(otherUser.getMemberName())).thenReturn(otherUser); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(otherUser.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.create(projectDTO); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherProjectIsCreated() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(null, "anotherCode", "otherName", false); + Project otherProject = new Project("anotherCode", "otherName"); + TestUtil.setField(otherProject, "id", 2L); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(otherProject); + Mockito.when(projectRepository.save(otherProject)).thenReturn(otherProject); + ProjectDTO result = new ProjectDTO(1l, "anotherCode", "otherName", false); + Mockito.when(genericMapper.map(otherProject, ProjectDTO.class)).thenReturn(result); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.create(projectDTO); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherProjectIsDeleted() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + Mockito.when(projectRepository.findOneByCode(otherProject.getCode())).thenReturn(otherProject); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.delete(otherProject.getCode()); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenUserAddedAsMemberOfAnotherProject() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + Mockito.when(projectRepository.findByContainerIdentifier(otherProject.getCode())).thenReturn(otherProject); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.addMember(otherProject.getCode(), new MemberDTO(USER.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherUserAddedAsMemberOfProject() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(userRepository.findByMemberName(otherUser.getMemberName())).thenReturn(otherUser); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.addMember(PROJECT.getCode(), new MemberDTO(otherUser.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenUserRoleIsChangedOnAnotherProject() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(otherProject.getCode(), USER.getMemberName())).thenReturn(new ProjectUserMember(otherProject, USER)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.updateMemberRole(otherProject.getCode(), USER.getMemberName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherUserRoleIsChangedOnProject() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), otherUser.getMemberName())).thenReturn(new ProjectUserMember(PROJECT, otherUser)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.updateMemberRole(PROJECT.getCode(), otherUser.getMemberName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenUserDeletedAsMemberOfAnotherProject() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(otherProject.getCode(), USER.getMemberName())).thenReturn(new ProjectUserMember(otherProject, USER)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.deleteMember(otherProject.getCode(), USER.getMemberName()); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherUserDeletedAsMemberOfProject() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), otherUser.getMemberName())).thenReturn(new ProjectUserMember(PROJECT, otherUser)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectUserMemberService.deleteMember(PROJECT.getCode(), otherUser.getMemberName()); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenGroupNotContainingUserIsAddedAsMemberOfProject() throws BadRequestException { + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(groupRepository.findByMemberName(GROUP.getName())).thenReturn(GROUP); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.addMember(PROJECT.getCode(), new MemberDTO(GROUP.getName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenGroupContainingUserIsAddedAsMemberOfAnotherProject() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + Mockito.when(projectRepository.findByContainerIdentifier(otherProject.getCode())).thenReturn(otherProject); + Mockito.when(groupRepository.findByMemberName(GROUP.getName())).thenReturn(GROUP); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.addMember(otherProject.getCode(), new MemberDTO(GROUP.getName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenGroupNotContainingUserRoleIsUpdatedOnProject() throws BadRequestException { + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(PROJECT, GROUP)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.updateMemberRole(PROJECT.getCode(), GROUP.getName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenGroupContainingUserRoleIsUpdatedOnAnotherProject() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(otherProject.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(otherProject, GROUP)); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.updateMemberRole(otherProject.getCode(), GROUP.getName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenGroupNotContainingUserIsDeletedAsMemberOfProject() throws BadRequestException { + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(PROJECT, GROUP)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.deleteMember(PROJECT.getCode(), GROUP.getName()); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenGroupContainingUserIsDeletedAsMemberOfAnotherProject() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(otherProject.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(otherProject, GROUP)); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectGroupMemberService.deleteMember(otherProject.getCode(), GROUP.getName()); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherUserIsAddedAsMemberOfGroup() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(groupRepository.findByContainerIdentifier(GROUP.getName())).thenReturn(GROUP); + Mockito.when(userRepository.findByMemberName(otherUser.getMemberName())).thenReturn(otherUser); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + groupMemberService.addMember(GROUP.getName(), new MemberDTO(otherUser.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherUserRoleIsUpdatedOnGroup() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName(GROUP.getName(), otherUser.getMemberName())).thenReturn(new GroupMember(GROUP, otherUser)); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + groupMemberService.updateMemberRole(GROUP.getName(), otherUser.getMemberName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherUserIsDeletedAsMemberOfGroup() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName(GROUP.getName(), otherUser.getMemberName())).thenReturn(new GroupMember(GROUP, otherUser)); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + groupMemberService.deleteMember(GROUP.getName(), otherUser.getMemberName()); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenProjectIsUpdatedAsDefaultProject() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(1L, PROJECT.getCode(), PROJECT.getName(), true); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.findOneByCode(PROJECT.getCode())).thenReturn(PROJECT); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.update(PROJECT.getCode(), projectDTO); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenAnotherProjectIsUpdatedAsDefaultProject() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(2L, "anotherCode", "otherName", true); + Project otherProject = new Project("anotherCode", "otherName"); + TestUtil.setField(otherProject, "id", 2L); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(otherProject); + Mockito.when(projectRepository.findOneByCode(otherProject.getCode())).thenReturn(otherProject); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.update(otherProject.getCode(), projectDTO); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenDefaultProjectIsUpdatedAsNotDefaultProject() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(1L, PROJECT.getCode(), PROJECT.getName(), false); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.findOneByCode(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(userPreferenceService.getValue(UserPreferenceService.DEFAULT_PROJECT)).thenReturn(PROJECT.getCode()); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.update(PROJECT.getCode(), projectDTO); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void securityServiceGetProjectMemberRolesShouldBeCachedAndNotClearedWhenProjectNotDefaultIsUpdatedAsNotDefaultProject() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(1L, PROJECT.getCode(), PROJECT.getName(), false); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.findOneByCode(PROJECT.getCode())).thenReturn(PROJECT); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + + projectService.update(PROJECT.getCode(), projectDTO); + + Assertions.assertEquals(1, securityService.getProjectMemberRoles(PROJECT.getCode(), USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenAProjectIsCreated() throws NotUniqueException { + ProjectDTO projectDTO = new ProjectDTO(null, "anotherCode", "otherName", false); + Project otherProject = new Project("anotherCode", "otherName"); + TestUtil.setField(otherProject, "id", 2L); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(otherProject); + Mockito.when(projectRepository.save(otherProject)).thenReturn(otherProject); + ProjectDTO result = new ProjectDTO(1l, "anotherCode", "otherName", false); + Mockito.when(genericMapper.map(otherProject, ProjectDTO.class)).thenReturn(result); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectService.create(projectDTO); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenAProjectIsUpdatedAsDefaultProject() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(1L, PROJECT.getCode(), PROJECT.getName(), true); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.findOneByCode(PROJECT.getCode())).thenReturn(PROJECT); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectService.update(PROJECT.getCode(), projectDTO); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenDefaultProjectIsUpdatedAsNotDefaultProject() throws BadRequestException { + Project otherProject = new Project("anotherCode", "otherName"); + TestUtil.setField(otherProject, "id", 2L); + ProjectDTO projectDTO = new ProjectDTO(2L, otherProject.getCode(), otherProject.getName(), false); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(otherProject); + Mockito.when(projectRepository.findOneByCode(otherProject.getCode())).thenReturn(otherProject); + Mockito.when(userPreferenceService.getValue(UserPreferenceService.DEFAULT_PROJECT)).thenReturn(otherProject.getCode()); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectService.update(otherProject.getCode(), projectDTO); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenAProjectIsDeleted() throws BadRequestException { + Mockito.when(projectRepository.findOneByCode(PROJECT.getCode())).thenReturn(PROJECT); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectService.delete(PROJECT.getCode()); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenUserAddedAsMemberOfAProject() throws BadRequestException { + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectUserMemberService.addMember(PROJECT.getCode(), new MemberDTO(USER.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenUserDeletedAsMemberOfAProject() throws BadRequestException { + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), USER.getMemberName())).thenReturn(new ProjectUserMember(PROJECT, USER)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectUserMemberService.deleteMember(PROJECT.getCode(), USER.getMemberName()); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenGroupContainingUserIsAddedAsMemberOfAProject() throws BadRequestException { + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(groupRepository.findByMemberName(GROUP.getName())).thenReturn(GROUP); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectGroupMemberService.addMember(PROJECT.getCode(), new MemberDTO(GROUP.getName(), MemberRole.ADMIN)); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenGroupContainingUserIsDeletedAsMemberOfAProject() throws BadRequestException { + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(PROJECT, GROUP)); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectGroupMemberService.deleteMember(PROJECT.getCode(), GROUP.getName()); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenUserIsAddedAsMemberOfGroup() throws BadRequestException { + Mockito.when(groupRepository.findByContainerIdentifier(GROUP.getName())).thenReturn(GROUP); + Mockito.when(userRepository.findByMemberName(USER.getMemberName())).thenReturn(USER); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + groupMemberService.addMember(GROUP.getName(), new MemberDTO(USER.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndClearedWhenUserIsDeletedAsMemberOfGroup() throws BadRequestException { + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName(GROUP.getName(), USER.getMemberName())).thenReturn(new GroupMember(GROUP, USER)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + groupMemberService.deleteMember(GROUP.getName(), USER.getMemberName()); + + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(2, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenProjectIsCreatedByAnotherUser() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + ProjectDTO projectDTO = new ProjectDTO(null, PROJECT.getCode(), PROJECT.getName(), false); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.save(PROJECT)).thenReturn(PROJECT); + ProjectDTO result = new ProjectDTO(1l, PROJECT.getCode(), PROJECT.getName(), true); + Mockito.when(genericMapper.map(PROJECT, ProjectDTO.class)).thenReturn(result); + Mockito.when(userRepository.findByMemberName(otherUser.getMemberName())).thenReturn(otherUser); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(otherUser.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectService.create(projectDTO); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenProjectNotDefaultIsUpdatedAsNotDefaultProject() throws BadRequestException { + ProjectDTO projectDTO = new ProjectDTO(1L, PROJECT.getCode(), PROJECT.getName(), false); + Mockito.when(genericMapper.map(projectDTO, Project.class)).thenReturn(PROJECT); + Mockito.when(projectRepository.findOneByCode(PROJECT.getCode())).thenReturn(PROJECT); + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication(USER.getMemberName(), Collections.emptyList())); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectService.update(PROJECT.getCode(), projectDTO); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenAnotherUserAddedAsMemberOfAProject() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(userRepository.findByMemberName(otherUser.getMemberName())).thenReturn(otherUser); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectUserMemberService.addMember(PROJECT.getCode(), new MemberDTO(otherUser.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenUserRoleIsChangedOnAProject() throws BadRequestException { + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), USER.getMemberName())).thenReturn(new ProjectUserMember(PROJECT, USER)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectUserMemberService.updateMemberRole(PROJECT.getCode(), USER.getMemberName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenAnotherUserDeletedAsMemberOfAProject() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(projectUserMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), otherUser.getMemberName())).thenReturn(new ProjectUserMember(PROJECT, otherUser)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectUserMemberService.deleteMember(PROJECT.getCode(), otherUser.getMemberName()); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenGroupNotContainingUserIsAddedAsMemberOfAProject() throws BadRequestException { + Mockito.when(projectRepository.findByContainerIdentifier(PROJECT.getCode())).thenReturn(PROJECT); + Mockito.when(groupRepository.findByMemberName(GROUP.getName())).thenReturn(GROUP); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectGroupMemberService.addMember(PROJECT.getCode(), new MemberDTO(GROUP.getName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenGroupContainingUserRoleIsUpdatedOnAProject() throws BadRequestException { + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(PROJECT, GROUP)); + Mockito.when(groupMemberRepository.findAllByIdGroupName(GROUP.getName())).thenReturn(List.of(new GroupMember(GROUP, USER))); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectGroupMemberService.updateMemberRole(PROJECT.getCode(), GROUP.getName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenGroupNotContainingUserIsDeletedAsMemberOfAProject() throws BadRequestException { + Mockito.when(projectGroupMemberRepository.findByContainerIdentifierAndMemberName(PROJECT.getCode(), GROUP.getName())).thenReturn(new ProjectGroupMember(PROJECT, GROUP)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + projectGroupMemberService.deleteMember(PROJECT.getCode(), GROUP.getName()); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenAnotherUserIsAddedAsMemberOfGroup() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(groupRepository.findByContainerIdentifier(GROUP.getName())).thenReturn(GROUP); + Mockito.when(userRepository.findByMemberName(otherUser.getMemberName())).thenReturn(otherUser); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + groupMemberService.addMember(GROUP.getName(), new MemberDTO(otherUser.getMemberName(), MemberRole.ADMIN)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenUserRoleIsUpdatedOnGroup() throws BadRequestException { + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName(GROUP.getName(), USER.getMemberName())).thenReturn(new GroupMember(GROUP, USER)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + groupMemberService.updateMemberRole(GROUP.getName(), USER.getMemberName(), MemberRole.ADMIN); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } + + @Test + void projectServiceFindAllShouldBeCachedAndNotClearedWhenAnotherUserIsDeletedAsMemberOfGroup() throws BadRequestException { + User otherUser = new User("otherName", "issuer"); + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName(GROUP.getName(), otherUser.getMemberName())).thenReturn(new GroupMember(GROUP, otherUser)); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + + groupMemberService.deleteMember(GROUP.getName(), otherUser.getMemberName()); + + Assertions.assertEquals(1, projectService.findAll(USER.getMemberName()).size()); + } +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/configuration/security/CustomSecurityTest.java b/code/api/api/src/test/java/com/decathlon/ara/configuration/security/CustomSecurityTest.java new file mode 100644 index 000000000..136a373f9 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/configuration/security/CustomSecurityTest.java @@ -0,0 +1,1093 @@ +package com.decathlon.ara.configuration.security; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors; +import org.springframework.security.web.FilterChainProxy; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.util.UriUtils; + +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.domain.enumeration.Permission; +import com.decathlon.ara.domain.enumeration.UserSecurityRole; +import com.decathlon.ara.service.security.SecurityService; +import com.decathlon.ara.web.rest.CommunicationResource; +import com.decathlon.ara.web.rest.CountryResource; +import com.decathlon.ara.web.rest.CycleDefinitionResource; +import com.decathlon.ara.web.rest.DemoResource; +import com.decathlon.ara.web.rest.ErrorResource; +import com.decathlon.ara.web.rest.ExecutedScenarioResource; +import com.decathlon.ara.web.rest.ExecutionResource; +import com.decathlon.ara.web.rest.FeatureResource; +import com.decathlon.ara.web.rest.FunctionalityResource; +import com.decathlon.ara.web.rest.GroupMemberResource; +import com.decathlon.ara.web.rest.GroupResource; +import com.decathlon.ara.web.rest.ProblemPatternResource; +import com.decathlon.ara.web.rest.ProblemResource; +import com.decathlon.ara.web.rest.ProjectAdministrationResource; +import com.decathlon.ara.web.rest.ProjectGroupMemberResource; +import com.decathlon.ara.web.rest.ProjectResource; +import com.decathlon.ara.web.rest.ProjectUserMemberResource; +import com.decathlon.ara.web.rest.PurgeResource; +import com.decathlon.ara.web.rest.RootCauseResource; +import com.decathlon.ara.web.rest.ScenarioResource; +import com.decathlon.ara.web.rest.SettingResource; +import com.decathlon.ara.web.rest.SourceResource; +import com.decathlon.ara.web.rest.TeamResource; +import com.decathlon.ara.web.rest.TemplateResource; +import com.decathlon.ara.web.rest.TypeResource; +import com.decathlon.ara.web.rest.audits.AuditingResource; +import com.decathlon.ara.web.rest.authentication.UserResource; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer; +import com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import com.github.tomakehurst.wiremock.matching.ContainsPattern; +import com.github.tomakehurst.wiremock.matching.EqualToPattern; +import com.nimbusds.jose.jwk.RSAKey; + +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.DefaultJwtBuilder; + +/** + * To test security, each api is called with different configuration of the Authentication, and then the status code is checked. + * Because spring check security before checking uri existence and to avoid mocking each controller's used services, no controller are loaded for the test. + * In this context, when security pass, the HTTP status code is 404. The problem of this way to test, is that if the uri doesn't exists and have no security configuration, we have a 404 too. + * To solve this problem a check based on classes annotation is done to check that the currently tested uri exists in the given classes + * @author z15lross + */ +@WebMvcTest +@ContextConfiguration(classes = { CustomSecurity.class }, initializers = CustomSecurityTest.Initializer.class) +class CustomSecurityTest { + + private static final Pattern STATE_EXTRACTOR_PATTERN = Pattern.compile("state=([^&]+)"); + private static final Pattern NONCE_EXTRACTOR_PATTERN = Pattern.compile("nonce=([^&]+)"); + + @RegisterExtension + private static WireMockExtension wireMockExtensionOidc = WireMockExtension.newInstance().options(WireMockConfiguration.wireMockConfig().dynamicPort().extensions(new ResponseTemplateTransformer(false)).usingFilesUnderClasspath("wiremock/oidc")).build(); + + @RegisterExtension + private static WireMockExtension wireMockExtensionOauth2 = WireMockExtension.newInstance().options(WireMockConfiguration.wireMockConfig().dynamicPort().extensions(new ResponseTemplateTransformer(false)).usingFilesUnderClasspath("wiremock/oauth2")).build(); + + private static KeyPair keyPair = generateRsaKey(); + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private FilterChainProxy springSecurityFilterChain; + + private MockMvc mockMvc; + + @MockBean + private SecurityService securityService; + + private static KeyPair generateRsaKey() { + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } + + static class Initializer implements ApplicationContextInitializer { + + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + TestPropertyValues.of(Map.of("spring.security.oauth2.client.provider.spring.issuer-uri", wireMockExtensionOidc.baseUrl(), + "spring.security.oauth2.client.registration.ara-client-oauth2.provider", "ara-provider-oauth2", + "spring.security.oauth2.client.registration.ara-client-oauth2.client-name", "ara-client-oauth2", + "spring.security.oauth2.client.registration.ara-client-oauth2.client-id", "ara-client", + "spring.security.oauth2.client.registration.ara-client-oauth2.client-secret", "ara-client", + "spring.security.oauth2.client.registration.ara-client-oauth2.authorization-grant-type", "authorization_code", + "spring.security.oauth2.client.registration.ara-client-oauth2.redirect-uri", "${ara.clientBaseUrl}/${ara.loginProcessingUrl}/{registrationId}", + "spring.security.oauth2.client.registration.ara-client-oauth2.scope", "profile", + "spring.security.oauth2.client.provider.ara-provider-oauth2.issuer-uri", wireMockExtensionOauth2.baseUrl(), + "spring.security.oauth2.resourceserver.jwt.issuer-uri", wireMockExtensionOauth2.baseUrl())).applyTo(applicationContext); + + } + } + + @BeforeEach + public void setup() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).addFilter(springSecurityFilterChain).build(); + } + + @Test + void shouldInitUserAndReturnCustomPrincipalWhenLoginWithOauth2Provider() throws Exception { + MockHttpSession session = new MockHttpSession(); + User user = login(session, false, "simple_user", "user1", null, Collections.emptyList()); + Authentication authentication = getAuthenticationfromSession(session); + OAuth2User principal = (OAuth2User) authentication.getPrincipal(); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals("com.decathlon.ara.configuration.security.CustomSecurity$AraOauth2User", principal.getClass().getName()); + Assertions.assertEquals("user1@email.com", principal.getAttribute("email")); + Assertions.assertTrue(authentication.getAuthorities().isEmpty()); + Mockito.verify(securityService).initUser(user.getName(), user.getIssuer()); + Mockito.verify(securityService).getUserRoles(user.getId()); + } + + @Test + void shouldInitUserAndReturnCustomPrincipalWhenLoginWithOidcProvider() throws Exception { + MockHttpSession session = new MockHttpSession(); + User user = login(session, true, "simple_user", "user1", null, Collections.emptyList()); + Authentication authentication = getAuthenticationfromSession(session); + OidcUser principal = (OidcUser) authentication.getPrincipal(); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals("com.decathlon.ara.configuration.security.CustomSecurity$AraOidcUser", principal.getClass().getName()); + Assertions.assertEquals("user1@email.com", principal.getEmail()); + Assertions.assertTrue(authentication.getAuthorities().isEmpty()); + Mockito.verify(securityService).initUser(user.getName(), user.getIssuer()); + Mockito.verify(securityService).getUserRoles(user.getId()); + } + + @Test + void shouldInitUserWhenUsingAppAsResourceServer() throws Exception { + MockHttpSession session = new MockHttpSession(); + User user = machineToMachineCall(session, Collections.emptyList()); + Authentication authentication = getAuthenticationfromSession(session); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertTrue(authentication.getAuthorities().isEmpty()); + Mockito.verify(securityService).initUser(user.getName(), user.getIssuer()); + Mockito.verify(securityService).getUserRoles(user.getId()); + } + + @Test + void authenticationShouldHaveAllUserInfoAttributeWhenLoginWithOauth2Provider() throws Exception { + MockHttpSession session = new MockHttpSession(); + Map attributes = Map.of("email_verified", "true", "birthdate", "01/01/1971", "customAttr", "value"); + User user = login(session, false, "user_with_attributes", "user1", attributes, Collections.emptyList()); + Authentication authentication = getAuthenticationfromSession(session); + OAuth2User principal = (OAuth2User) authentication.getPrincipal(); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals("user1@email.com", principal.getAttribute("email")); + attributes.forEach((key, value) -> Assertions.assertEquals(value, principal.getAttribute(key))); + } + + @Test + void principalShouldHaveAllIdTokenClaimsWhenLoginWithOidcProvider() throws Exception { + MockHttpSession session = new MockHttpSession(); + Map attributes = Map.of("email_verified", "true", "birthdate", "01/01/1971", "customAttr", "value"); + User user = login(session, true, "user_with_attributes", "user1", attributes, Collections.emptyList()); + Authentication authentication = getAuthenticationfromSession(session); + OidcUser principal = (OidcUser) authentication.getPrincipal(); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals("user1@email.com", principal.getEmail()); + Assertions.assertEquals(true, principal.getEmailVerified()); + Assertions.assertEquals("01/01/1971", principal.getBirthdate()); + Assertions.assertEquals("value", principal.getAttribute("customAttr")); + } + + @Test + void authenticationShouldHaveAuthoritiesGetFromDatabaseWhenLoginWithOauth2Provider() throws Exception { + MockHttpSession session = new MockHttpSession(); + List userRoles = List.of(UserSecurityRole.ADMIN, UserSecurityRole.PROJECT_OR_GROUP_CREATOR); + User user = login(session, false, "user_with_authorities", "user1", null, userRoles); + Authentication authentication = getAuthenticationfromSession(session); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals(userRoles.size(), authentication.getAuthorities().size()); + List authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(); + userRoles.forEach(userRole -> Assertions.assertTrue(authorities.contains("ROLE_" + userRole.name()))); + } + + @Test + void authenticationShouldHaveAuthoritiesGetFromDatabaseWhenLoginWithOidcProvider() throws Exception { + MockHttpSession session = new MockHttpSession(); + List userRoles = List.of(UserSecurityRole.ADMIN, UserSecurityRole.PROJECT_OR_GROUP_CREATOR); + User user = login(session, true, "user_with_authorities", "user1", null, userRoles); + Authentication authentication = getAuthenticationfromSession(session); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals(userRoles.size(), authentication.getAuthorities().size()); + List authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(); + userRoles.forEach(userRole -> Assertions.assertTrue(authorities.contains("ROLE_" + userRole.name()))); + } + + @Test + void authenticationShouldHaveAuthoritiesGetFromDatabaseWhenUsingAppAsResourceServer() throws Exception { + MockHttpSession session = new MockHttpSession(); + List userRoles = List.of(UserSecurityRole.ADMIN, UserSecurityRole.PROJECT_OR_GROUP_CREATOR); + User user = machineToMachineCall(session, userRoles); + Authentication authentication = getAuthenticationfromSession(session); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals(user.getId(), authentication.getName()); + Assertions.assertEquals(userRoles.size(), authentication.getAuthorities().size()); + List authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(); + userRoles.forEach(userRole -> Assertions.assertTrue(authorities.contains("ROLE_" + userRole.name()))); + } + + private static Stream provideUnauthenticatedTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.GET, "/api/admin/projects", ProjectAdministrationResource.class), + Arguments.of(HttpMethod.GET, "/api/auditing/users-roles", AuditingResource.class), + Arguments.of(HttpMethod.POST, "/api/demo", DemoResource.class), + Arguments.of(HttpMethod.DELETE, "/api/demo", DemoResource.class), + Arguments.of(HttpMethod.GET, "/api/users", UserResource.class), + Arguments.of(HttpMethod.GET, "/api/users/toto", UserResource.class), + Arguments.of(HttpMethod.GET, "/api/users/current/details", UserResource.class), + Arguments.of(HttpMethod.GET, "/api/groups", GroupResource.class), + Arguments.of(HttpMethod.POST, "/api/groups", GroupResource.class), + Arguments.of(HttpMethod.GET, "/api/groups/toto", GroupResource.class), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto", GroupResource.class), + Arguments.of(HttpMethod.GET, "/api/projects", ProjectResource.class), + Arguments.of(HttpMethod.POST, "/api/projects", ProjectResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto", ProjectResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications", CommunicationResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications/titi", CommunicationResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/communications/titi", CommunicationResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/countries", CountryResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/countries", CountryResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/countries/ok", CountryResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/countries/ok", CountryResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/42", ErrorResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/errors/matching", ErrorResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/distinct/titi", ErrorResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executed-scenarios/history", ExecutedScenarioResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executed-scenarios/42", ExecutedScenarioResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions", ExecutionResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest", ExecutionResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/request-completion", ExecutionResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/quality-status", ExecutionResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest-eligible-versions", ExecutionResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/upload", ExecutionResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42", ExecutionResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/with-successes", ExecutionResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/discard", ExecutionResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/un-discard", ExecutionResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/history", ExecutionResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/42/filtered", ExecutionResource.class), + Arguments.of(HttpMethod.GET, "/api/features", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/features/titi", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/features/titi/state", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/features/titi/default", FeatureResource.class), + Arguments.of(HttpMethod.PATCH, "/api/features", FeatureResource.class), + Arguments.of(HttpMethod.PATCH, "/api/features/titi", FeatureResource.class), + Arguments.of(HttpMethod.DELETE, "/api/features", FeatureResource.class), + Arguments.of(HttpMethod.DELETE, "/api/features/titi", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities", FunctionalityResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/functionalities/42", FunctionalityResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities", FunctionalityResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities", FunctionalityResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities/42", FunctionalityResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move", FunctionalityResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move/list", FunctionalityResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/42/scenarios", FunctionalityResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/coverage", FunctionalityResource.class), + Arguments.of(HttpMethod.OPTIONS, "/api/projects/toto/functionalities/export", FunctionalityResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/export", FunctionalityResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/import", FunctionalityResource.class), + Arguments.of(HttpMethod.GET, "/api/groups/toto/members", GroupMemberResource.class), + Arguments.of(HttpMethod.GET, "/api/groups/toto/members/titi", GroupMemberResource.class), + Arguments.of(HttpMethod.POST, "/api/groups/toto/members", GroupMemberResource.class), + Arguments.of(HttpMethod.PATCH, "/api/groups/toto/members/titi", GroupMemberResource.class), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto/members/titi", GroupMemberResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problem-patterns/42/errors", ProblemPatternResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems", ProblemResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42", ProblemResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42/errors", ProblemResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problems/42", ProblemResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/filter", ProblemResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/append-pattern", ProblemResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42", ProblemResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/pick-up-pattern/41", ProblemResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/close/41", ProblemResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/reopen", ProblemResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/refresh-defect-status", ProblemResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/recompute-first-and-last-seen-date-times", ProblemResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users", ProjectUserMemberResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/users", ProjectUserMemberResource.class), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/purge/force", PurgeResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/root-causes", RootCauseResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/root-causes/42", RootCauseResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/root-causes", RootCauseResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/root-causes/42", RootCauseResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload/toto", ScenarioResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload-postman/toto", ScenarioResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/without-functionalities", ScenarioResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/ignored", ScenarioResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/toto", SettingResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings", SettingResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings/technology", SettingResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/titi/technology/tutu", SettingResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/sources", SourceResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/sources", SourceResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/sources/titi", SourceResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/sources/titi", SourceResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/teams", TeamResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/teams/42", TeamResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/teams", TeamResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/teams/42", TeamResource.class), + Arguments.of(HttpMethod.GET, "/api/templates/cycle-execution", TemplateResource.class), + Arguments.of(HttpMethod.POST, "/api/projects/toto/types", TypeResource.class), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/types/titi", TypeResource.class), + Arguments.of(HttpMethod.GET, "/api/projects/toto/types", TypeResource.class), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/types/titi", TypeResource.class)); + } + + @ParameterizedTest + @MethodSource("provideUnauthenticatedTestArguments") + void shouldHasUnauthorizedwhenUnAuthenticated(HttpMethod method, String uri, Class controllerClass) throws Exception { + mockMvc.perform(MockMvcRequestBuilders.request(method, uri)).andExpect(MockMvcResultMatchers.status().isUnauthorized()); + assertRequestExists(controllerClass, method, uri); + } + + private static Stream provideAuthenticatedTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.GET, "/api/users", UserResource.class), + Arguments.of(HttpMethod.GET, "/api/users/current/details", UserResource.class), + Arguments.of(HttpMethod.GET, "/api/groups", GroupResource.class), + Arguments.of(HttpMethod.GET, "/api/groups/toto", GroupResource.class), + Arguments.of(HttpMethod.GET, "/api/projects", ProjectResource.class), + Arguments.of(HttpMethod.GET, "/api/features", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/features/titi", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/features/titi/state", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/features/titi/default", FeatureResource.class), + Arguments.of(HttpMethod.GET, "/api/groups/toto/members", GroupMemberResource.class), + Arguments.of(HttpMethod.GET, "/api/groups/toto/members/titi", GroupMemberResource.class), + Arguments.of(HttpMethod.GET, "/api/templates/cycle-execution", TemplateResource.class)); + } + + @ParameterizedTest + @MethodSource("provideAuthenticatedTestArguments") + void shouldHasNotFoundwhenAuthenticated(HttpMethod method, String uri, Class controllerClass) throws Exception { + TestAuthentication authentication = new TestAuthentication(Collections.emptyList()); + mockMvc.perform(MockMvcRequestBuilders.request(method, uri).with(SecurityMockMvcRequestPostProcessors.authentication(authentication))).andExpect(MockMvcResultMatchers.status().isNotFound()); + assertRequestExists(controllerClass, method, uri); + } + + private static Stream provideAuthenticatedWithUserWithOtherRoleThanTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.GET, "/api/admin/projects", ProjectAdministrationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/auditing/users-roles", AuditingResource.class, UserSecurityRole.AUDITING), + Arguments.of(HttpMethod.POST, "/api/demo", DemoResource.class, UserSecurityRole.PROJECT_OR_GROUP_CREATOR), + Arguments.of(HttpMethod.DELETE, "/api/demo", DemoResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/users/toto", UserResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/groups", GroupResource.class, UserSecurityRole.PROJECT_OR_GROUP_CREATOR), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto", GroupResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects", ProjectResource.class, UserSecurityRole.PROJECT_OR_GROUP_CREATOR), + Arguments.of(HttpMethod.PUT, "/api/projects/toto", ProjectResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications", CommunicationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/42", ErrorResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/errors/matching", ErrorResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/distinct/titi", ErrorResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executed-scenarios/history", ExecutedScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executed-scenarios/42", ExecutedScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/request-completion", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/quality-status", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest-eligible-versions", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/upload", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/with-successes", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/discard", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/un-discard", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/history", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/42/filtered", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/features", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/features/titi", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/features", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/features/titi", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move/list", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/42/scenarios", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/coverage", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.OPTIONS, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/import", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/groups/toto/members", GroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problem-patterns/42/errors", ProblemPatternResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42/errors", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/filter", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/append-pattern", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/pick-up-pattern/41", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/close/41", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/reopen", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/refresh-defect-status", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/recompute-first-and-last-seen-date-times", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/purge/force", PurgeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload/toto", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload-postman/toto", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/without-functionalities", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/ignored", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/toto", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings/technology", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/titi/technology/tutu", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN)); + } + + @ParameterizedTest + @MethodSource("provideAuthenticatedWithUserWithOtherRoleThanTestArguments") + void shouldHasForbiddenwhenAuthenticatedWithUserWithOtherRoleThan(HttpMethod method, String uri, Class controllerClass, UserSecurityRole role) throws Exception { + TestAuthentication authentication = new TestAuthentication(allRolesExcept(role)); + mockMvc.perform(MockMvcRequestBuilders.request(method, uri).with(SecurityMockMvcRequestPostProcessors.authentication(authentication))).andExpect(MockMvcResultMatchers.status().isForbidden()); + assertRequestExists(controllerClass, method, uri); + } + + private static Stream provideAuthenticatedWithUserWithRoleTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.GET, "/api/admin/projects", ProjectAdministrationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/auditing/users-roles", AuditingResource.class, UserSecurityRole.AUDITING), + Arguments.of(HttpMethod.POST, "/api/demo", DemoResource.class, UserSecurityRole.PROJECT_OR_GROUP_CREATOR), + Arguments.of(HttpMethod.DELETE, "/api/demo", DemoResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/users/toto", UserResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/groups", GroupResource.class, UserSecurityRole.PROJECT_OR_GROUP_CREATOR), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto", GroupResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects", ProjectResource.class, UserSecurityRole.PROJECT_OR_GROUP_CREATOR), + Arguments.of(HttpMethod.PUT, "/api/projects/toto", ProjectResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications", CommunicationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/42", ErrorResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/errors/matching", ErrorResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/distinct/titi", ErrorResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executed-scenarios/history", ExecutedScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executed-scenarios/42", ExecutedScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/request-completion", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/quality-status", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest-eligible-versions", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/upload", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/with-successes", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/discard", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/un-discard", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/history", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/42/filtered", ExecutionResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/features", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/features/titi", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/features", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/features/titi", FeatureResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move/list", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/42/scenarios", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/coverage", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.OPTIONS, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/import", FunctionalityResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/groups/toto/members", GroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problem-patterns/42/errors", ProblemPatternResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42/errors", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/filter", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/append-pattern", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/pick-up-pattern/41", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/close/41", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/reopen", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/refresh-defect-status", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/recompute-first-and-last-seen-date-times", ProblemResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/purge/force", PurgeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload/toto", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload-postman/toto", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/without-functionalities", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/ignored", ScenarioResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/toto", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings/technology", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/titi/technology/tutu", SettingResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN)); + } + + @ParameterizedTest + @MethodSource("provideAuthenticatedWithUserWithRoleTestArguments") + void shouldHasNotFoundwhenAuthenticatedWithUserWithRole(HttpMethod method, String uri, Class controllerClass, UserSecurityRole role) throws Exception { + TestAuthentication authentication = new TestAuthentication(roles(role)); + mockMvc.perform(MockMvcRequestBuilders.request(method, uri).with(SecurityMockMvcRequestPostProcessors.authentication(authentication))).andExpect(MockMvcResultMatchers.status().isNotFound()); + assertRequestExists(controllerClass, method, uri); + } + + private static Stream provideAuthenticatedWithUserWithOtherRoleThanAndProjecMemberWithPermissionOtherThanTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.PUT, "/api/projects/toto", ProjectResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications", CommunicationResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/42", ErrorResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/errors/matching", ErrorResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/distinct/titi", ErrorResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executed-scenarios/history", ExecutedScenarioResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executed-scenarios/42", ExecutedScenarioResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/request-completion", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/quality-status", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest-eligible-versions", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/upload", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/with-successes", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/discard", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/un-discard", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/history", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/42/filtered", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move/list", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/42/scenarios", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/coverage", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.OPTIONS, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/import", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problem-patterns/42/errors", ProblemPatternResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42/errors", ProblemResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/filter", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/append-pattern", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/pick-up-pattern/41", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/close/41", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/reopen", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/refresh-defect-status", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/recompute-first-and-last-seen-date-times", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/purge/force", PurgeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload/toto", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload-postman/toto", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/without-functionalities", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/ignored", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/toto", SettingResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings", SettingResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings/technology", SettingResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/titi/technology/tutu", SettingResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN)); + } + + @ParameterizedTest + @MethodSource("provideAuthenticatedWithUserWithOtherRoleThanAndProjecMemberWithPermissionOtherThanTestArguments") + void shouldHasForbiddenwhenAuthenticatedWithUserWithOtherRoleThanAndProjectMemberWithPermissionOtherThan(HttpMethod method, String uri, Class controllerClass, UserSecurityRole role, Permission permission) throws Exception { + TestAuthentication authentication = new TestAuthentication(allRolesExcept(role)); + Mockito.when(securityService.getProjectMemberRoles("toto", authentication.getName())).thenReturn(allMemberRolesExceptPermission(permission)); + mockMvc.perform(MockMvcRequestBuilders.request(method, uri).with(SecurityMockMvcRequestPostProcessors.authentication(authentication))).andExpect(MockMvcResultMatchers.status().isForbidden()); + assertRequestExists(controllerClass, method, uri); + } + + private static Stream provideAuthenticatedWithUserWithOtherRoleThanAndProjectMemberWithPermissionTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.PUT, "/api/projects/toto", ProjectResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications", CommunicationResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/communications/titi", CommunicationResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/countries", CountryResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/countries/ok", CountryResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/cycle-definitions", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/cycle-definitions/42", CycleDefinitionResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/42", ErrorResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/errors/matching", ErrorResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/errors/distinct/titi", ErrorResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executed-scenarios/history", ExecutedScenarioResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executed-scenarios/42", ExecutedScenarioResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/request-completion", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/quality-status", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/latest-eligible-versions", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/upload", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/with-successes", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/discard", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/executions/42/un-discard", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/executions/42/history", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/executions/42/filtered", ExecutionResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/functionalities/42", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/move/list", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/42/scenarios", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/coverage", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.OPTIONS, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/functionalities/export", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/functionalities/import", FunctionalityResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problem-patterns/42/errors", ProblemPatternResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problem-patterns/42", ProblemPatternResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/problems/42/errors", ProblemResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/filter", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/append-pattern", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/42/pick-up-pattern/41", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/close/41", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/reopen", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/problems/42/refresh-defect-status", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/projects/toto/problems/recompute-first-and-last-seen-date-times", ProblemResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/groups", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/groups/titi", ProjectGroupMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/members/users", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PATCH, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/members/users/titi", ProjectUserMemberResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/purge/force", PurgeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/root-causes", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/root-causes/42", RootCauseResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload/toto", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/scenarios/upload-postman/toto", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/without-functionalities", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/scenarios/ignored", ScenarioResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/toto", SettingResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings", SettingResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.GET, "/api/projects/toto/settings/technology", SettingResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/settings/titi/technology/tutu", SettingResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.POST, "/api/projects/toto/sources", SourceResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/sources/titi", SourceResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/teams", TeamResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/teams/42", TeamResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.POST, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.PUT, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN), + Arguments.of(HttpMethod.GET, "/api/projects/toto/types", TypeResource.class, UserSecurityRole.ADMIN, Permission.READ), + Arguments.of(HttpMethod.DELETE, "/api/projects/toto/types/titi", TypeResource.class, UserSecurityRole.ADMIN, Permission.ADMIN)); + } + + @ParameterizedTest + @MethodSource("provideAuthenticatedWithUserWithOtherRoleThanAndProjectMemberWithPermissionTestArguments") + void shouldHasNodFoundwhenAuthenticatedWithUserWithOtherRoleThanAndProjectMemberWithPermission(HttpMethod method, String uri, Class controllerClass, UserSecurityRole role, Permission permission) throws Exception { + TestAuthentication authentication = new TestAuthentication(allRolesExcept(role)); + Mockito.when(securityService.getProjectMemberRoles("toto", authentication.getName())).thenReturn(memberRoleWithAtLeast(permission)); + mockMvc.perform(MockMvcRequestBuilders.request(method, uri).with(SecurityMockMvcRequestPostProcessors.authentication(authentication))).andExpect(MockMvcResultMatchers.status().isNotFound()); + assertRequestExists(controllerClass, method, uri); + } + + private static Stream provideAuthenticatedWithUserWithOtherRoleThanAndGroupMemberWithPermissionOtherThanTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.DELETE, "/api/groups/toto", GroupResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/groups/toto/members", GroupMemberResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PATCH, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN, Permission.WRITE)); + } + + @ParameterizedTest + @MethodSource("provideAuthenticatedWithUserWithOtherRoleThanAndGroupMemberWithPermissionOtherThanTestArguments") + void shouldHasForbiddenResponseWhenAuthenticatedWithUserWithOtherRoleThanAndGroupMemberWithPermissionOtherThan(HttpMethod method, String uri, Class controllerClass, UserSecurityRole role, Permission permission) throws Exception { + TestAuthentication authentication = new TestAuthentication(allRolesExcept(role)); + Mockito.when(securityService.getGroupMemberRoles("toto", authentication.getName())).thenReturn(allMemberRolesExceptPermission(permission)); + mockMvc.perform(MockMvcRequestBuilders.request(method, uri).with(SecurityMockMvcRequestPostProcessors.authentication(authentication))).andExpect(MockMvcResultMatchers.status().isForbidden()); + assertRequestExists(controllerClass, method, uri); + } + + private static Stream provideAuthenticatedWithUserWithOtherRoleThanAndGroupMemberWithPermissionTestArguments() { + return Stream.of( + Arguments.of(HttpMethod.DELETE, "/api/groups/toto", GroupResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.POST, "/api/groups/toto/members", GroupMemberResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.PATCH, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN, Permission.WRITE), + Arguments.of(HttpMethod.DELETE, "/api/groups/toto/members/titi", GroupMemberResource.class, UserSecurityRole.ADMIN, Permission.WRITE)); + } + + @ParameterizedTest + @MethodSource("provideAuthenticatedWithUserWithOtherRoleThanAndGroupMemberWithPermissionTestArguments") + void shouldHasNotFoundResponseWhenAuthenticatedWithUserWithOtherRoleThanAndGroupMemberWithPermission(HttpMethod method, String uri, Class controllerClass, UserSecurityRole role, Permission permission) throws Exception { + TestAuthentication authentication = new TestAuthentication(allRolesExcept(role)); + Mockito.when(securityService.getGroupMemberRoles("toto", authentication.getName())).thenReturn(memberRoleWithAtLeast(permission)); + mockMvc.perform(MockMvcRequestBuilders.request(method, uri).with(SecurityMockMvcRequestPostProcessors.authentication(authentication))).andExpect(MockMvcResultMatchers.status().isNotFound()); + assertRequestExists(controllerClass, method, uri); + } + + private User machineToMachineCall(MockHttpSession session, List userRoles) throws Exception { + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + mockJwks(wireMockExtensionOauth2, publicKey, privateKey); + JwtBuilder jwtBuilder = new DefaultJwtBuilder().setSubject("user1").setIssuer(wireMockExtensionOauth2.baseUrl()).setAudience("ara-client").setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + 3600000)).signWith(SignatureAlgorithm.RS256, privateKey); + String accessToken = jwtBuilder.compact(); + User user = new User("user1", wireMockExtensionOauth2.baseUrl()); + Mockito.when(securityService.initUser("user1", wireMockExtensionOauth2.baseUrl())).thenReturn(user); + Mockito.when(securityService.getUserRoles(user.getId())).thenReturn(userRoles); + mockMvc.perform(MockMvcRequestBuilders.get("/").header("Authorization", "Bearer " + accessToken).session(session)).andReturn(); + return user; + } + + private User login(MockHttpSession session, boolean oidc, String code, String userName, Map attributes, List userRoles) throws Exception { + Map userAttributes = attributes == null ? new HashMap<>() : new HashMap<>(attributes); + userAttributes.put("email", userName + "@email.com"); + WireMockExtension wireMockExtension = null; + String registrationId = null; + if (oidc) { + wireMockExtension = wireMockExtensionOidc; + registrationId = "ara-client-oidc"; + } else { + wireMockExtension = wireMockExtensionOauth2; + registrationId = "ara-client-oauth2"; + } + + MvcResult loginResult = mockMvc.perform(MockMvcRequestBuilders.get("/oauth/login/" + registrationId).session(session)).andReturn(); + String location = loginResult.getResponse().getHeader("Location"); + Matcher stateMatcher = STATE_EXTRACTOR_PATTERN.matcher(location); + String state = null; + String nonce = null; + if (stateMatcher.find()) { + state = stateMatcher.group(1); + state = UriUtils.decode(state, StandardCharsets.UTF_8); + if (oidc) { + Matcher nonceMatcher = NONCE_EXTRACTOR_PATTERN.matcher(location); + if (nonceMatcher.find()) { + nonce = nonceMatcher.group(1); + nonce = UriUtils.decode(nonce, StandardCharsets.UTF_8); + } + } + } + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + mockJwks(wireMockExtension, publicKey, privateKey); + JwtBuilder jwtBuilder = new DefaultJwtBuilder().setSubject(userName).setIssuer(wireMockExtension.baseUrl()).setAudience("ara-client").setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + 3600000)).signWith(SignatureAlgorithm.RS256, privateKey); + String accessToken = jwtBuilder.compact(); + StringBuilder tokenEndpointBody = new StringBuilder("{\"access_token\": \""); + tokenEndpointBody.append(accessToken).append("\",\"token_type\": \"Bearer\", \"expire_in\": 3600"); + if (oidc) { + tokenEndpointBody.append(",\"id_token\":\""); + jwtBuilder.claim("nonce", nonce); + for (Map.Entry entry : userAttributes.entrySet()) { + jwtBuilder.claim(entry.getKey(), entry.getValue()); + } + tokenEndpointBody.append(jwtBuilder.compact()).append('"'); + } + tokenEndpointBody.append('}'); + wireMockExtension.stubFor(WireMock.post("/oauth2/token").withRequestBody(new ContainsPattern("code=" + code)).willReturn(WireMock.okForContentType(MediaType.APPLICATION_JSON_VALUE, tokenEndpointBody.toString()))); + if (!oidc) { + StringBuilder userInfoEndpointBody = new StringBuilder("{\"sub\":\""); + userInfoEndpointBody.append(userName).append('"'); + for (Map.Entry entry : userAttributes.entrySet()) { + userInfoEndpointBody.append(",\"").append(entry.getKey()).append("\":\"").append(entry.getValue()).append('"'); + } + userInfoEndpointBody.append('}'); + wireMockExtension.stubFor(WireMock.get("/oauth2/userinfo").withHeader("Authorization", new ContainsPattern(accessToken)).willReturn(WireMock.okForContentType(MediaType.APPLICATION_JSON_VALUE, userInfoEndpointBody.toString()))); + } + User user = new User(userName, wireMockExtension.baseUrl()); + Mockito.when(securityService.initUser(userName, wireMockExtension.baseUrl())).thenReturn(user); + Mockito.when(securityService.getUserRoles(user.getId())).thenReturn(userRoles); + mockMvc.perform(MockMvcRequestBuilders.get("/oauth/logincomplete/" + registrationId + "?code=" + code + "&state=" + state).session(session)).andReturn(); + return user; + } + + private void mockJwks(WireMockExtension wireMockExtension, RSAPublicKey publicKey, RSAPrivateKey privateKey) { + if (wireMockExtension.findAllStubsByMetadata(new EqualToPattern("/oauth2/jwks")).getMappings().isEmpty()) { + wireMockExtension.stubFor(WireMock.get("/oauth2/jwks").willReturn(WireMock.okForContentType(MediaType.APPLICATION_JSON_VALUE, "{\"keys\":[" + new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build().toPublicJWK().toJSONString() + "]}"))); + } + } + + private Authentication getAuthenticationfromSession(MockHttpSession session) { + return ((SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT")).getAuthentication(); + } + + private List allRolesExcept(UserSecurityRole... roles) { + Set except = new HashSet<>(); + for (UserSecurityRole role : roles) { + except.add(role); + UserSecurityRole parent = role; + while ((parent = parent.getParent()) != null) { + except.add(parent); + } + } + return roles(Arrays.stream(UserSecurityRole.values()).filter(role -> !except.contains(role)).toList()); + } + + private List roles(UserSecurityRole... roles) { + return roles(Arrays.asList(roles)); + } + + private List roles(List roles) { + return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role.name())).toList(); + } + + private Set memberRoleWithAtLeast(Permission permission) { + Set memberRoles = new HashSet<>(); + Optional roleWithPermission = Arrays.stream(MemberRole.values()).filter(role -> role.hasPermission(permission)).findFirst(); + if (roleWithPermission.isPresent()) { + memberRoles.add(roleWithPermission.get()); + } + return memberRoles; + } + + private Set allMemberRolesExceptPermission(Permission... permissions) { + return Arrays.stream(MemberRole.values()).filter(role -> { + for (Permission permission : permissions) { + if (role.hasPermission(permission)) { + return false; + } + } + return true; + }).collect(Collectors.toSet()); + } + + private void assertRequestExists(Class controllerClass, HttpMethod httpMethod, String uri) { + RequestMapping requestMapping = controllerClass.getAnnotation(RequestMapping.class); + if (requestMapping == null) { + Assertions.fail(controllerClass.getName() + " is not a RequestMapping"); + } + String[] baseUris = requestMapping.value(); + for (Method method : controllerClass.getMethods()) { + requestMapping = method.getAnnotation(RequestMapping.class); + if (requestMapping != null) { + for (RequestMethod requestMethod : requestMapping.method()) { + if (requestMethod.name().equals(httpMethod.name())) { + for (String baseUri : baseUris) { + if (match(uri, baseUri, requestMapping.value())) { + return; + } + } + } + } + } else { + for (Annotation annotation : method.getAnnotations()) { + requestMapping = annotation.annotationType().getAnnotation(RequestMapping.class); + if (requestMapping != null) { + for (RequestMethod requestMethod : requestMapping.method()) { + if (requestMethod.name().equals(httpMethod.name())) { + for (String baseUri : baseUris) { + try { + String[] paths = (String[]) annotation.annotationType().getMethod("value").invoke(annotation); + if (match(uri, baseUri, paths)) { + return; + } + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { + Assertions.fail("should not happen", e); + } + } + } + } + } + } + } + } + Assertions.fail(httpMethod + " " + uri + " doesn't match any method in " + controllerClass.getName()); + } + + private boolean match(String uri, String baseUri, String[] paths) { + if (paths.length > 0) { + for (String path : paths) { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (antPathMatcher.match(baseUri + path, uri)) { + return true; + } + } + } else { + AntPathMatcher antPathMatcher = new AntPathMatcher(); + if (antPathMatcher.match(baseUri, uri)) { + return true; + } + } + return false; + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/configuration/security/TestAuthentication.java b/code/api/api/src/test/java/com/decathlon/ara/configuration/security/TestAuthentication.java new file mode 100644 index 000000000..4d4a0683a --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/configuration/security/TestAuthentication.java @@ -0,0 +1,77 @@ +package com.decathlon.ara.configuration.security; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +public class TestAuthentication implements Authentication, OAuth2User { + + private static final long serialVersionUID = 1L; + + private String userName; + private List authorities; + + public TestAuthentication() { + this(Collections.emptyList()); + } + + public TestAuthentication(List authorities) { + this("userName", authorities); + } + + public TestAuthentication(String userName, List authorities) { + this.userName = userName; + this.authorities = authorities; + } + + @Override + public String getName() { + return userName; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getDetails() { + return null; + } + + @Override + public Object getPrincipal() { + return this; + } + + @Override + public boolean isAuthenticated() { + return true; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + } + + @Override + public Map getAttributes() { + return null; + } + + @Override + public A getAttribute(String name) { + return null; + } + +} \ No newline at end of file diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/DemoServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/DemoServiceTest.java index 7e16d7b36..65ab4cb3b 100644 --- a/code/api/api/src/test/java/com/decathlon/ara/service/DemoServiceTest.java +++ b/code/api/api/src/test/java/com/decathlon/ara/service/DemoServiceTest.java @@ -17,29 +17,38 @@ package com.decathlon.ara.service; -import com.decathlon.ara.loader.*; -import com.decathlon.ara.repository.ProjectRepository; -import com.decathlon.ara.service.dto.project.ProjectDTO; -import com.decathlon.ara.service.exception.BadRequestException; -import com.decathlon.ara.service.exception.NotFoundException; +import static com.decathlon.ara.loader.DemoLoaderConstants.PROJECT_CODE_DEMO; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Optional; - -import static com.decathlon.ara.loader.DemoLoaderConstants.PROJECT_CODE_DEMO; -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.when; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.decathlon.ara.configuration.security.TestAuthentication; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.loader.DemoExecutionLoader; +import com.decathlon.ara.loader.DemoFunctionalityLoader; +import com.decathlon.ara.loader.DemoProblemLoader; +import com.decathlon.ara.loader.DemoScenarioLoader; +import com.decathlon.ara.loader.DemoSettingsLoader; +import com.decathlon.ara.service.dto.member.MemberDTO; +import com.decathlon.ara.service.dto.project.ProjectDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; @ExtendWith(MockitoExtension.class) class DemoServiceTest { - @Mock - private ProjectRepository projectRepository; - @Mock private ProjectService projectService; @@ -60,23 +69,35 @@ class DemoServiceTest { @Mock private DemoSettingsLoader demoSettingsLoader; + + @Mock + private ProjectUserMemberService projectUserMemberService; @InjectMocks private DemoService cut; @Test - void create_ShouldFail_WhenDemoProjectAlreadyExists() { + void create_ShouldAddAdminRoleToCurrentUser_WhenDemoProjectAlreadyExists() throws BadRequestException { // GIVEN + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); when(projectService.findOne(PROJECT_CODE_DEMO)).thenReturn(Optional.of(new ProjectDTO())); // WHEN - assertThrows(BadRequestException.class, () -> cut.create()); + cut.create(); + + //THEN + ArgumentCaptor captor = ArgumentCaptor.forClass(MemberDTO.class); + verify(projectUserMemberService).addMember(eq(PROJECT_CODE_DEMO), captor.capture()); + MemberDTO member = captor.getValue(); + assertEquals(authentication.getName(), member.getName()); + assertEquals(MemberRole.ADMIN, member.getRole()); } @Test - void delete_ShouldFail_WhenDemoProjectDoesNotExist() { + void delete_ShouldFail_WhenDemoProjectDoesNotExist() throws NotFoundException { // GIVEN - when(projectRepository.findOneByCode(PROJECT_CODE_DEMO)).thenReturn(null); + when(projectService.toId(PROJECT_CODE_DEMO)).thenThrow(NotFoundException.class); // WHEN assertThrows(NotFoundException.class, () -> cut.delete()); diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/GroupMemberServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/GroupMemberServiceTest.java new file mode 100644 index 000000000..ae6410321 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/GroupMemberServiceTest.java @@ -0,0 +1,81 @@ +package com.decathlon.ara.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.MemberContainerRepository; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.GroupRepository; +import com.decathlon.ara.repository.MemberRelationshipRepository; +import com.decathlon.ara.repository.MemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.util.TestUtil; + +@ExtendWith(MockitoExtension.class) +public class GroupMemberServiceTest extends MemberServiceTest{ + + @Mock + private GroupRepository groupRepository; + @Mock + private GroupMemberRepository groupMemberRepository; + @Mock + private UserRepository userRepository; + + @InjectMocks + private GroupMemberService groupMemberService; + + @Override + protected MemberService getMemberService() { + return groupMemberService; + } + + @Override + protected MemberContainerRepository getMemberContainerRepository() { + return groupRepository; + } + + @Override + protected MemberRelationshipRepository getMemberRelationshipRepository() { + return groupMemberRepository; + } + + @Override + protected MemberRepository getMemberRepository() { + return userRepository; + } + + @Override + protected GroupMember contructMemberRelationship(String memberName, MemberRole role) { + GroupMember groupMember = new GroupMember(new Group(), contructMember(memberName)); + groupMember.setRole(role); + return groupMember; + } + + @Override + protected Group contructContainer(String name) { + return new Group(name); + } + + @Override + protected User contructMember(String memberName) { + User user = new User(); + TestUtil.setField(user, "id", memberName); + return user; + } + + @Test + @Override + void addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist() throws BadRequestException { + addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist((container, member, relationship) -> Assertions.assertEquals(container, relationship.getGroup())); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/GroupServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/GroupServiceTest.java new file mode 100644 index 000000000..7eed37c83 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/GroupServiceTest.java @@ -0,0 +1,137 @@ +package com.decathlon.ara.service; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.decathlon.ara.configuration.security.TestAuthentication; +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.GroupRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.dto.group.GroupDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; +import com.decathlon.ara.service.exception.NotUniqueException; +import com.decathlon.ara.util.TestUtil; + +@ExtendWith(MockitoExtension.class) +class GroupServiceTest { + + @Mock + private GroupRepository groupRepository; + @Mock + private UserRepository userRepository; + @Mock + private GroupMemberRepository groupMemberRepository; + @Mock + private ProjectGroupMemberRepository projectGroupMemberRepository; + + @InjectMocks + private GroupService groupService; + + @Test + void findAllShouldReturnEmptyListWhenNoGroupsExists() { + Assertions.assertEquals(0, groupRepository.findAll().size()); + } + + @Test + void findAllShouldReturnListContaningAllDatabaseGroup() { + List databaseGroups = List.of(new Group("groupA"), + new Group("groupB"), + new Group("groupC")); + + Mockito.when(groupRepository.findAll()).thenReturn(databaseGroups); + List resultList = groupService.findAll(); + Assertions.assertEquals(databaseGroups.size(), resultList.size()); + for (int i=0; i< databaseGroups.size(); i++) { + Assertions.assertEquals(databaseGroups.get(i).getMemberName(), resultList.get(i).getName()); + } + } + + @Test + void findOneShouldThrowNotFoundExceptionWhenGroupDoesntExist() throws NotFoundException { + Assertions.assertThrows(NotFoundException.class, () -> groupService.findOne("name")); + } + + @Test + void findOneShouldReturnGroupDTOWhenGroupExist() throws NotFoundException { + Group group = new Group("name"); + Mockito.when(groupRepository.findByMemberName("name")).thenReturn(group); + GroupDTO result = groupService.findOne("name"); + Assertions.assertEquals(group.getMemberName(), result.getName()); + } + + @Test + void createShouldThrowNotUniqueExceptionWhenGroupAlreadyExists() { + Mockito.when(groupRepository.findByMemberName("name")).thenReturn(new Group("name")); + Assertions.assertThrows(NotUniqueException.class, ()-> groupService.create(new GroupDTO("name"))); + Mockito.verify(groupRepository, Mockito.times(0)).save(Mockito.any()); + } + + @Test + void createShouldCreateGroupAndAddCurrentUserAsAdminWhenGroupNotAlreadyExists() throws NotUniqueException { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + User user = new User(); + TestUtil.setField(user, "id", authentication.getName()); + Mockito.when(userRepository.findByMemberName(authentication.getName())).thenReturn(user); + GroupDTO groupDto = new GroupDTO("name"); + GroupDTO result = groupService.create(groupDto); + ArgumentCaptor groupCaptor = ArgumentCaptor.forClass(Group.class); + Mockito.verify(groupRepository).save(groupCaptor.capture()); + Group savedGroup = groupCaptor.getValue(); + Assertions.assertEquals(groupDto, result); + Assertions.assertEquals(groupDto.getName(), savedGroup.getName()); + ArgumentCaptor groupMemberCaptor = ArgumentCaptor.forClass(GroupMember.class); + Mockito.verify(groupMemberRepository).save(groupMemberCaptor.capture()); + GroupMember savedGroupMember = groupMemberCaptor.getValue(); + Assertions.assertEquals(groupDto.getName(), savedGroupMember.getGroupName()); + Assertions.assertEquals(user.getId(), savedGroupMember.getMemberName()); + Assertions.assertEquals(MemberRole.ADMIN, savedGroupMember.getRole()); + } + + @Test + void deletehouldThrowNotFoundExceptionWhenGroupDoesntExist() { + Assertions.assertThrows(NotFoundException.class, () -> groupService.delete("name")); + Mockito.verify(groupRepository, Mockito.times(0)).delete(Mockito.any()); + Mockito.verify(groupMemberRepository, Mockito.times(0)).deleteByIdGroupName(Mockito.any()); + } + + @Test + void deleteShouldThrowBadRequestExceptionWhenGroupIsMemberOfProjects() throws BadRequestException { + Group group = new Group("name"); + Mockito.when(groupRepository.findByMemberName("name")).thenReturn(group); + Mockito.when(projectGroupMemberRepository.findAllByIdMemberName("name")).thenReturn(List.of(new ProjectGroupMember(new Project(), group))); + Assertions.assertThrows(BadRequestException.class, () -> groupService.delete("name")); + Mockito.verify(groupRepository, Mockito.times(0)).delete(Mockito.any()); + Mockito.verify(groupMemberRepository, Mockito.times(0)).deleteByIdGroupName(Mockito.any()); + } + + @Test + void deleteShouldDeleteGroupAndAllItsMemberWhenItExists() throws BadRequestException { + Group group = new Group("name"); + Mockito.when(groupRepository.findByMemberName("name")).thenReturn(group); + groupService.delete("name"); + ArgumentCaptor captor = ArgumentCaptor.forClass(Group.class); + Mockito.verify(groupRepository).delete(captor.capture()); + Group deletedGroup = captor.getValue(); + Assertions.assertEquals(group, deletedGroup); + Mockito.verify(groupMemberRepository).deleteByIdGroupName("name"); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/MemberServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/MemberServiceTest.java new file mode 100644 index 000000000..819de015f --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/MemberServiceTest.java @@ -0,0 +1,159 @@ +package com.decathlon.ara.service; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; + +import com.decathlon.ara.cache.CacheService; +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.MemberContainerRepository; +import com.decathlon.ara.domain.MemberRelationship; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.MemberRelationshipRepository; +import com.decathlon.ara.repository.MemberRepository; +import com.decathlon.ara.service.dto.member.MemberDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; +import com.decathlon.ara.util.TestUtil; + +abstract class MemberServiceTest> { + + protected abstract MemberService getMemberService(); + protected abstract MemberContainerRepository getMemberContainerRepository(); + protected abstract MemberRelationshipRepository getMemberRelationshipRepository(); + protected abstract MemberRepository getMemberRepository(); + protected abstract C contructContainer(String name); + protected abstract M contructMember(String memberName); + protected abstract R contructMemberRelationship(String memberName, MemberRole role); + + @Mock + protected CacheService cacheService; + + @Test + void findAllShouldReturnEmptyListWhenNoRelationshipExists() { + Assertions.assertEquals(0, getMemberService().findAll("identifier").size()); + } + + @Test + void findAllShouldReturnListContaningAllDatabaseRelationship() { + List databaseRelatioship = List.of( + contructMemberRelationship("adminA", MemberRole.ADMIN), + contructMemberRelationship("maintainerA", MemberRole.MAINTAINER), + contructMemberRelationship("memberA", MemberRole.MEMBER), + contructMemberRelationship("memberB", MemberRole.MEMBER)); + + Mockito.when(getMemberRelationshipRepository().findAllByContainerIdentifier("identifier")).thenReturn(databaseRelatioship); + List resultList = getMemberService().findAll("identifier"); + Assertions.assertEquals(databaseRelatioship.size(), resultList.size()); + for (int i=0; i< databaseRelatioship.size(); i++) { + Assertions.assertEquals(databaseRelatioship.get(i).getMemberName(), resultList.get(i).getName()); + Assertions.assertEquals(databaseRelatioship.get(i).getRole(), resultList.get(i).getRole()); + } + } + + @Test + void findOneShouldThrowNotFoundExceptionWhenMemberRelationshipDoesntExist() throws NotFoundException { + Assertions.assertThrows(NotFoundException.class, () -> getMemberService().findOne("identifier", "memberName")); + } + + @Test + void findOneShouldReturnMemberDTOWhenMemberRelationshipExist() throws NotFoundException { + R relationship = contructMemberRelationship("memberName", MemberRole.ADMIN); + Mockito.when(getMemberRelationshipRepository().findByContainerIdentifierAndMemberName("identifier", "memberName")).thenReturn(relationship); + MemberDTO result = getMemberService().findOne("identifier", "memberName"); + Assertions.assertEquals(relationship.getMemberName(), result.getName()); + Assertions.assertEquals(relationship.getRole(), result.getRole()); + } + + @Test + void addMemberShouldThrowBadRequestExceptionExceptionWhenRelationshipAlreadyExist() { + Mockito.when(getMemberRelationshipRepository().findByContainerIdentifierAndMemberName("identifier", "memberName")).thenReturn(contructMemberRelationship("test", MemberRole.ADMIN)); + Assertions.assertThrows(BadRequestException.class, () -> getMemberService().addMember("identifier", new MemberDTO("memberName", null))); + Mockito.verify(getMemberRelationshipRepository(), Mockito.times(0)).save(Mockito.any()); + } + + @Test + void addMemberShouldThrowNotFoundExceptionWhenRelationshipNotExistAndContainerNotExist() { + Assertions.assertThrows(NotFoundException.class, () -> getMemberService().addMember("identifier", new MemberDTO())); + Mockito.verify(getMemberRelationshipRepository(), Mockito.times(0)).save(Mockito.any()); + } + + @Test + void addMemberShouldThrowNotFoundExceptionWhenRelationshipNotExistAndContainerExistAndMemberNotExist() { + Mockito.when(getMemberContainerRepository().findByContainerIdentifier("identifier")).thenReturn(contructContainer("name")); + Assertions.assertThrows(NotFoundException.class, () -> getMemberService().addMember("identifier", new MemberDTO())); + Mockito.verify(getMemberRelationshipRepository(), Mockito.times(0)).save(Mockito.any()); + } + + @Test + void addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist() throws BadRequestException { + addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist(null); + } + + void addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist(AdditionalAssertion additionalAssertion) throws BadRequestException { + C container = contructContainer("name"); + Mockito.when(getMemberContainerRepository().findByContainerIdentifier("identifier")).thenReturn(container); + M member = contructMember("memberName"); + Mockito.when(getMemberRepository().findByMemberName("memberName")).thenReturn(member); + MemberDTO memberDTO = new MemberDTO("memberName", MemberRole.MEMBER); + Class RelationshipClass = TestUtil.getField(getMemberService(), "relationshipClass"); + MemberDTO result = getMemberService().addMember("identifier", memberDTO); + Assertions.assertEquals(memberDTO, result); + ArgumentCaptor captor = ArgumentCaptor.forClass(RelationshipClass); + Mockito.verify(getMemberRelationshipRepository()).save(captor.capture()); + R savedRelationship = captor.getValue(); + Assertions.assertEquals(savedRelationship.getMemberName(), member.getMemberName()); + Assertions.assertEquals(savedRelationship.getRole(), memberDTO.getRole()); + if (additionalAssertion != null) { + additionalAssertion.assertFor(container, member, savedRelationship); + } + } + + @Test + void updateMemberRoleShouldThrowNotFoundExceptionWhenRelationshipNotExist() { + Assertions.assertThrows(NotFoundException.class, () -> getMemberService().updateMemberRole("identifier", "memberName", null)); + Mockito.verify(getMemberRelationshipRepository(), Mockito.times(0)).save(Mockito.any()); + } + + @Test + void updateMemberRoleShouldUpdateRelationshipWhenRelationshipExists() throws BadRequestException { + R relationship = contructMemberRelationship("memberName", MemberRole.ADMIN); + Mockito.when(getMemberRelationshipRepository().findByContainerIdentifierAndMemberName("identifier", "memberName")).thenReturn(relationship); + Class relationshipClass = TestUtil.getField(getMemberService(), "relationshipClass"); + MemberDTO result = getMemberService().updateMemberRole("identifier", "memberName", MemberRole.MAINTAINER); + Assertions.assertEquals("memberName", result.getName()); + Assertions.assertEquals(MemberRole.MAINTAINER, result.getRole()); + ArgumentCaptor captor = ArgumentCaptor.forClass(relationshipClass); + Mockito.verify(getMemberRelationshipRepository()).save(captor.capture()); + R updated = captor.getValue(); + Assertions.assertEquals(result.getName(), updated.getMemberName()); + Assertions.assertEquals(result.getRole(), updated.getRole()); + } + + @Test + void deleteMemberShouldThrowNotFoundExceptionWhenRelationshipNotExist() { + Assertions.assertThrows(NotFoundException.class, () -> getMemberService().deleteMember("identifier", "memberName")); + } + + @Test + void deleteMemberShouldDeleteRelationshipWhenRelationshipExist() throws BadRequestException { + R relationship = contructMemberRelationship("memberName", MemberRole.ADMIN); + Mockito.when(getMemberRelationshipRepository().findByContainerIdentifierAndMemberName("identifier", "memberName")).thenReturn(relationship); + Class RelationshipClass = TestUtil.getField(getMemberService(), "relationshipClass"); + getMemberService().deleteMember("identifier", "memberName"); + ArgumentCaptor captor = ArgumentCaptor.forClass(RelationshipClass); + Mockito.verify(getMemberRelationshipRepository()).delete(captor.capture()); + R deleted = captor.getValue(); + Assertions.assertEquals(relationship, deleted); + } + + interface AdditionalAssertion{ + + public void assertFor(C container, M member, R Relationship); + + } +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/ProjectGroupMemberServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/ProjectGroupMemberServiceTest.java new file mode 100644 index 000000000..5f3166784 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/ProjectGroupMemberServiceTest.java @@ -0,0 +1,81 @@ +package com.decathlon.ara.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.MemberContainerRepository; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.GroupRepository; +import com.decathlon.ara.repository.MemberRelationshipRepository; +import com.decathlon.ara.repository.MemberRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.ProjectRepository; +import com.decathlon.ara.service.exception.BadRequestException; + +@ExtendWith(MockitoExtension.class) +public class ProjectGroupMemberServiceTest extends MemberServiceTest{ + + @Mock + private ProjectRepository projectRepository; + @Mock + private ProjectGroupMemberRepository projectGroupMemberRepository; + @Mock + private GroupRepository groupRepository; + @Mock + private GroupMemberRepository groupMemberRepository; + + @InjectMocks + private ProjectGroupMemberService projectGroupMemberService; + + @Override + protected MemberService getMemberService() { + return projectGroupMemberService; + } + + @Override + protected MemberContainerRepository getMemberContainerRepository() { + return projectRepository; + } + + @Override + protected MemberRelationshipRepository getMemberRelationshipRepository() { + return projectGroupMemberRepository; + } + + @Override + protected MemberRepository getMemberRepository() { + return groupRepository; + } + + @Override + protected Project contructContainer(String name) { + return new Project(name, name); + } + + @Override + protected Group contructMember(String memberName) { + return new Group(memberName); + } + + @Override + protected ProjectGroupMember contructMemberRelationship(String memberName, MemberRole role) { + ProjectGroupMember projectGroupMember = new ProjectGroupMember(new Project(), contructMember(memberName)); + projectGroupMember.setRole(role); + return projectGroupMember; + } + + @Test + @Override + void addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist() throws BadRequestException { + addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist((container, member, relationship) -> Assertions.assertEquals(container, relationship.getProject())); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/ProjectUserMemberServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/ProjectUserMemberServiceTest.java new file mode 100644 index 000000000..d749b40c1 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/ProjectUserMemberServiceTest.java @@ -0,0 +1,81 @@ +package com.decathlon.ara.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.MemberContainerRepository; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.repository.MemberRelationshipRepository; +import com.decathlon.ara.repository.MemberRepository; +import com.decathlon.ara.repository.ProjectRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.util.TestUtil; + +@ExtendWith(MockitoExtension.class) +public class ProjectUserMemberServiceTest extends MemberServiceTest{ + + @Mock + private ProjectRepository projectRepository; + @Mock + private ProjectUserMemberRepository projectUserMemberRepository; + @Mock + private UserRepository userRepository; + + @InjectMocks + private ProjectUserMemberService projectUserMemberService; + + @Override + protected MemberService getMemberService() { + return projectUserMemberService; + } + + @Override + protected MemberContainerRepository getMemberContainerRepository() { + return projectRepository; + } + + @Override + protected MemberRelationshipRepository getMemberRelationshipRepository() { + return projectUserMemberRepository; + } + + @Override + protected MemberRepository getMemberRepository() { + return userRepository; + } + + @Override + protected Project contructContainer(String name) { + return new Project(name, name); + } + + @Override + protected User contructMember(String memberName) { + User user = new User(); + TestUtil.setField(user, "id", memberName); + return user; + } + + @Override + protected ProjectUserMember contructMemberRelationship(String memberName, MemberRole role) { + ProjectUserMember projectUserMember = new ProjectUserMember(new Project(), contructMember(memberName)); + projectUserMember.setRole(role); + return projectUserMember; + } + + @Test + @Override + void addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist() throws BadRequestException { + addMemberShouldCreateMemberWhenRelationshipNotExistAndContainerExistAndMemberExist((container, member, relationship) -> Assertions.assertEquals(container, relationship.getProject())); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/UserPreferenceServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/UserPreferenceServiceTest.java new file mode 100644 index 000000000..356062420 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/UserPreferenceServiceTest.java @@ -0,0 +1,174 @@ +package com.decathlon.ara.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.decathlon.ara.configuration.security.TestAuthentication; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.UserPreference; +import com.decathlon.ara.repository.UserPreferenceRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.UserPreferenceService.Preference; + +@ExtendWith(MockitoExtension.class) +class UserPreferenceServiceTest { + + @Mock + private UserPreferenceRepository userPreferenceRepository; + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserPreferenceService userPreferenceService; + + @Test + void getValueShouldReturnDefaultValueWhenPreferenceIsNotSetForUser() { + SecurityContextHolder.getContext().setAuthentication(new TestAuthentication()); + Preference preference = new Preference("test", "default"); + String value = userPreferenceService.getValue(preference); + Assertions.assertEquals(preference.defaultValue(), value); + } + + @Test + void getValueShouldReturnDatabaseValueWhenPreferenceIsSetForUser() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", "default"); + UserPreference userPreference = new UserPreference(); + userPreference.setValue("database"); + Mockito.when(userPreferenceRepository.findByIdUserIdAndIdKey(authentication.getName(), preference.key())).thenReturn(userPreference); + String value = userPreferenceService.getValue(preference); + Assertions.assertEquals(userPreference.getValue(), value); + } + + @Test + void isEnabledShouldReturnDefaultWhenPreferenceIsNotSetForUserAndDefaultValueIsABoolean() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", Boolean.TRUE); + boolean enabled = userPreferenceService.isEnabled(preference); + Assertions.assertTrue(enabled); + } + + @Test + void isEnabledShouldReturnBooleanValueOfDefaultValueWhenPreferenceIsNotSetForUserAndDefaultValueIsAStringReprensentationOfABoolean() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", Boolean.TRUE.toString()); + boolean enabled = userPreferenceService.isEnabled(preference); + Assertions.assertTrue(enabled); + } + + @Test + void isEnabledShouldReturnFalseWhenPreferenceIsNotSetForUserAndDefaultValueIsNotABooleanOrStringRepresentationOfABoolean() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", "notABooleanValue"); + boolean enabled = userPreferenceService.isEnabled(preference); + Assertions.assertFalse(enabled); + } + + @Test + void isEnabledShouldReturnFalseWhenPreferenceIsNotSetForUserAndDefaultValueIsNull() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", null); + boolean enabled = userPreferenceService.isEnabled(preference); + Assertions.assertFalse(enabled); + } + + @Test + void isEnabledShouldReturnFalseWhenPreferenceIsSetForUserWithNotAStringReprensentationOfABoolean() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", Boolean.TRUE); + UserPreference userPreference = new UserPreference(); + userPreference.setValue("notABooleanValue"); + Mockito.when(userPreferenceRepository.findByIdUserIdAndIdKey(authentication.getName(), preference.key())).thenReturn(userPreference); + boolean enabled = userPreferenceService.isEnabled(preference); + Assertions.assertFalse(enabled); + } + + @Test + void isEnabledShouldReturnBooleanValueOfDatabaseValueWhenPreferenceIsSetForUserWithStringReprensentationOfABoolean() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", Boolean.FALSE); + UserPreference userPreference = new UserPreference(); + userPreference.setValue("true"); + Mockito.when(userPreferenceRepository.findByIdUserIdAndIdKey(authentication.getName(), preference.key())).thenReturn(userPreference); + boolean enabled = userPreferenceService.isEnabled(preference); + Assertions.assertTrue(enabled); + } + + @Test + void setValueShouldCreateUserPreferenceIfItNotAlreadyExists() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", "default"); + User user = new User(); + Mockito.when(userRepository.findByMemberName(authentication.getName())).thenReturn(user); + userPreferenceService.setValue(preference, "value"); + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + Mockito.verify(userPreferenceRepository).save(captor.capture()); + UserPreference savedPreference = captor.getValue(); + Assertions.assertEquals(user, savedPreference.getUser()); + Assertions.assertEquals(preference.key(), savedPreference.getKey()); + Assertions.assertEquals("value", savedPreference.getValue()); + } + + @Test + void setValueShouldUpdateUserPreferenceIfItAlreadyExists() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", "default"); + UserPreference userPreference = new UserPreference(); + Mockito.when(userPreferenceRepository.findByIdUserIdAndIdKey(authentication.getName(), preference.key())).thenReturn(userPreference); + userPreferenceService.setValue(preference, "value"); + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + Mockito.verify(userPreferenceRepository).save(captor.capture()); + UserPreference savedPreference = captor.getValue(); + Assertions.assertEquals(userPreference, savedPreference); + Assertions.assertEquals("value", savedPreference.getValue()); + } + + @Test + void setValueShouldSetStringRepresentationOfValueObjectWhenValueIsNotAString() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", Boolean.FALSE); + User user = new User(); + Mockito.when(userRepository.findByMemberName(authentication.getName())).thenReturn(user); + userPreferenceService.setValue(preference, Boolean.TRUE); + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + Mockito.verify(userPreferenceRepository).save(captor.capture()); + UserPreference savedPreference = captor.getValue(); + Assertions.assertEquals(user, savedPreference.getUser()); + Assertions.assertEquals(preference.key(), savedPreference.getKey()); + Assertions.assertEquals(Boolean.TRUE.toString(), savedPreference.getValue()); + } + + @Test + void setValueShouldSetNullValueWhenValueIsNull() { + TestAuthentication authentication = new TestAuthentication(); + SecurityContextHolder.getContext().setAuthentication(authentication); + Preference preference = new Preference("test", Boolean.FALSE); + User user = new User(); + Mockito.when(userRepository.findByMemberName(authentication.getName())).thenReturn(user); + userPreferenceService.setValue(preference, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(UserPreference.class); + Mockito.verify(userPreferenceRepository).save(captor.capture()); + UserPreference savedPreference = captor.getValue(); + Assertions.assertEquals(user, savedPreference.getUser()); + Assertions.assertEquals(preference.key(), savedPreference.getKey()); + Assertions.assertNull(savedPreference.getValue()); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/UserServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/UserServiceTest.java new file mode 100644 index 000000000..fe8438a12 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/UserServiceTest.java @@ -0,0 +1,62 @@ +package com.decathlon.ara.service; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.service.dto.group.GroupDTO; +import com.decathlon.ara.service.dto.user.UserDTO; +import com.decathlon.ara.service.exception.NotFoundException; + +@ExtendWith(MockitoExtension.class) +class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @InjectMocks + private UserService userService; + + @Test + void findAllShouldReturnEmptyListWhenNoUsersExists() { + Assertions.assertEquals(0, userRepository.findAll().size()); + } + + @Test + void findAllShouldReturnListContaningAllDatabaseUser() { + List databaseUsers = List.of(new User("userA", "issuerA"), + new User("userB", "issuerA"), + new User("userA", "issuerB"), + new User("userC", "issuerB")); + + Mockito.when(userRepository.findAll()).thenReturn(databaseUsers); + List resultList = userService.findAll(); + Assertions.assertEquals(databaseUsers.size(), resultList.size()); + for (int i=0; i< databaseUsers.size(); i++) { + Assertions.assertEquals(databaseUsers.get(i).getId(), resultList.get(i).getName()); + Assertions.assertNull(resultList.get(i).getRoles()); + } + } + + @Test + void findOneShouldThrowNotFoundExceptionWhenUserDoesntExist() throws NotFoundException { + Assertions.assertThrows(NotFoundException.class, () -> userService.findOne("userName")); + } + + @Test + void findOneShouldReturnUserDTOWhenUserExist() throws NotFoundException { + User user = new User("name", "issuer"); + Mockito.when(userRepository.findByMemberName(user.getId())).thenReturn(user); + UserDTO result = userService.findOne(user.getId()); + Assertions.assertEquals(user.getMemberName(), result.getName()); + } +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/auditing/AuditingServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/auditing/AuditingServiceTest.java new file mode 100644 index 000000000..70db4760d --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/auditing/AuditingServiceTest.java @@ -0,0 +1,164 @@ +package com.decathlon.ara.service.auditing; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.UserRole; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.domain.enumeration.UserSecurityRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.repository.UserRoleRepository; +import com.decathlon.ara.service.dto.auditing.MemberRoleDetails; +import com.decathlon.ara.service.dto.auditing.ProjectRoleDetails; +import com.decathlon.ara.service.dto.auditing.UserRoleDetails; + +@ExtendWith(MockitoExtension.class) +class AuditingServiceTest { + + @Mock + private UserRepository userRepository; + @Mock(lenient = true) + private UserRoleRepository userRoleRepository; + @Mock(lenient = true) + private GroupMemberRepository groupMemberRepository; + @Mock(lenient = true) + private ProjectUserMemberRepository projectUserMemberRepository; + @Mock(lenient = true) + private ProjectGroupMemberRepository projectGroupMemberRepository; + + @InjectMocks + private AuditingService auditingService; + + @Test + void auditUsersRolesShouldReturnEmptyListIfNoUserExists() { + Assertions.assertTrue(auditingService.auditUsersRoles().isEmpty()); + } + + @Test + void auditUsersRolesShouldReturnEmptyListWithAllUserWhenNoUserHasRight() { + User user1 = new User("user1", "issuer"); + User user2 = new User("user2", "issuer"); + Mockito.when(userRepository.findAll()).thenReturn(List.of(user1, user2)); + + List auditUsersRoles = auditingService.auditUsersRoles(); + + Assertions.assertEquals(2, auditUsersRoles.size()); + Assertions.assertEquals(user1.getMemberName(), auditUsersRoles.get(0).getUserName()); + Assertions.assertTrue(auditUsersRoles.get(0).getRoles().isEmpty()); + Assertions.assertTrue(auditUsersRoles.get(0).getProjects().isEmpty()); + Assertions.assertEquals(user2.getMemberName(), auditUsersRoles.get(1).getUserName()); + Assertions.assertTrue(auditUsersRoles.get(1).getRoles().isEmpty()); + Assertions.assertTrue(auditUsersRoles.get(1).getProjects().isEmpty()); + } + + @Test + void auditUsersRolesShouldReturnDetailsWithMultipleUserAndRoles() { + User userWithOnlyGlobalRoles = new User("user1", "issuer"); + User userWithOnlyProjectsRoles = new User("user2", "issuer"); + User userWithComplexRoles = new User("user3", "issuer"); + + Project project1 = new Project("project1", "project1"); + Project project2 = new Project("project2", "project2"); + Project project3 = new Project("project3", "project3"); + + Group group1 = new Group("group1"); + Group group2 = new Group("group2"); + + Mockito.when(userRepository.findAll()).thenReturn(List.of(userWithOnlyGlobalRoles, userWithOnlyProjectsRoles, userWithComplexRoles)); + + Mockito.when(userRoleRepository.findAllByIdUserId(userWithOnlyGlobalRoles.getMemberName())).thenReturn(List.of(new UserRole(userWithOnlyGlobalRoles, UserSecurityRole.ADMIN))); + Mockito.when(userRoleRepository.findAllByIdUserId(userWithComplexRoles.getMemberName())).thenReturn(List.of(new UserRole(userWithComplexRoles, UserSecurityRole.PROJECT_OR_GROUP_CREATOR), new UserRole(userWithComplexRoles, UserSecurityRole.AUDITING))); + + Mockito.when(groupMemberRepository.findAllByIdUserName(userWithOnlyProjectsRoles.getMemberName())).thenReturn(List.of(new GroupMember(group1, userWithOnlyProjectsRoles))); + Mockito.when(groupMemberRepository.findAllByIdUserName(userWithComplexRoles.getMemberName())).thenReturn(List.of(new GroupMember(group1, userWithComplexRoles), new GroupMember(group2, userWithComplexRoles))); + + ProjectGroupMember project1group1Member = new ProjectGroupMember(project1, group1); + project1group1Member.setRole(MemberRole.ADMIN); + + ProjectGroupMember project2group2Member = new ProjectGroupMember(project2, group2); + project2group2Member.setRole(MemberRole.MEMBER); + + Mockito.when(projectGroupMemberRepository.findAllByIdMemberName(group1.getName())).thenReturn(List.of(project1group1Member)); + Mockito.when(projectGroupMemberRepository.findAllByIdMemberName(group2.getName())).thenReturn(List.of(project2group2Member)); + + ProjectUserMember project1member1 = new ProjectUserMember(project1, userWithOnlyProjectsRoles); + project1member1.setRole(MemberRole.MAINTAINER); + + ProjectUserMember project1member2 = new ProjectUserMember(project1, userWithComplexRoles); + project1member2.setRole(MemberRole.ADMIN); + + ProjectUserMember project3member1 = new ProjectUserMember(project3, userWithComplexRoles); + project3member1.setRole(MemberRole.ADMIN); + + Mockito.when(projectUserMemberRepository.findAllByIdMemberName(userWithOnlyProjectsRoles.getMemberName())).thenReturn(List.of(project1member1)); + Mockito.when(projectUserMemberRepository.findAllByIdMemberName(userWithComplexRoles.getMemberName())).thenReturn(List.of(project1member2, project3member1)); + + List auditUsersRoles = auditingService.auditUsersRoles(); + + List expected = new ArrayList<>(); + UserRoleDetails userRoleDetails = new UserRoleDetails(userWithOnlyGlobalRoles.getMemberName()); + userRoleDetails.addRoles(UserSecurityRole.ADMIN); + expected.add(userRoleDetails); + userRoleDetails = new UserRoleDetails(userWithOnlyProjectsRoles.getMemberName()); + ProjectRoleDetails projectRoleDetails = userRoleDetails.getProject(project1.getCode()); + projectRoleDetails.addRole(MemberRole.ADMIN, group1.getName()); + projectRoleDetails.addRole(MemberRole.MAINTAINER, null); + expected.add(userRoleDetails); + userRoleDetails = new UserRoleDetails(userWithComplexRoles.getMemberName()); + userRoleDetails.addRoles(UserSecurityRole.PROJECT_OR_GROUP_CREATOR); + userRoleDetails.addRoles(UserSecurityRole.AUDITING); + projectRoleDetails = userRoleDetails.getProject(project1.getCode()); + projectRoleDetails.addRole(MemberRole.ADMIN, group1.getName()); + projectRoleDetails.addRole(MemberRole.ADMIN, null); + projectRoleDetails = userRoleDetails.getProject(project2.getCode()); + projectRoleDetails.addRole(MemberRole.MEMBER, group2.getName()); + projectRoleDetails = userRoleDetails.getProject(project3.getCode()); + projectRoleDetails.addRole(MemberRole.ADMIN, null); + expected.add(userRoleDetails); + + Assertions.assertEquals(expected.size(), auditUsersRoles.size()); + for (int i = 0; i < auditUsersRoles.size(); i++) { + checkDetails(expected.get(i), auditUsersRoles.get(i)); + } + } + + public void checkDetails(UserRoleDetails expected, UserRoleDetails result) { + Assertions.assertEquals(expected.getUserName(), result.getUserName()); + Assertions.assertEquals(expected.getRoles(), result.getRoles()); + Assertions.assertEquals(expected.getProjects().size(), result.getProjects().size()); + for (int i = 0; i < result.getProjects().size(); i++) { + checkDetails(expected.getProjects().get(i), result.getProjects().get(i)); + } + } + + public void checkDetails(ProjectRoleDetails expected, ProjectRoleDetails result) { + Assertions.assertEquals(expected.getCode(), result.getCode()); + Assertions.assertEquals(expected.getRoles().size(), result.getRoles().size()); + for (int i = 0; i < result.getRoles().size(); i++) { + checkDetails(expected.getRoles().get(i), result.getRoles().get(i)); + } + } + + public void checkDetails(MemberRoleDetails expected, MemberRoleDetails result) { + Assertions.assertEquals(expected.getMemberRole(), result.getMemberRole()); + Assertions.assertEquals(expected.getInheritFromGroup(), result.getInheritFromGroup()); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/service/security/SecurityServiceTest.java b/code/api/api/src/test/java/com/decathlon/ara/service/security/SecurityServiceTest.java new file mode 100644 index 000000000..cfee1dc0d --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/service/security/SecurityServiceTest.java @@ -0,0 +1,287 @@ +package com.decathlon.ara.service.security; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.UserRole; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.domain.enumeration.UserSecurityRole; +import com.decathlon.ara.repository.GroupMemberRepository; +import com.decathlon.ara.repository.ProjectGroupMemberRepository; +import com.decathlon.ara.repository.ProjectUserMemberRepository; +import com.decathlon.ara.repository.UserRepository; +import com.decathlon.ara.repository.UserRoleRepository; +import com.decathlon.ara.util.TestUtil; + +@ExtendWith(MockitoExtension.class) +class SecurityServiceTest { + + @Mock + private ProjectUserMemberRepository projectUserMemberRepository; + @Mock + private ProjectGroupMemberRepository projectGroupMemberRepository; + @Mock + private UserRepository userRepository; + @Mock + private UserRoleRepository userRoleRepository; + @Mock + private GroupMemberRepository groupMemberRepository; + + private SecurityService securityService; + + private void prepareTest(boolean adminCreated, UserSecurityRole newUserRole, String firstAdminName) { + if (adminCreated) { + Mockito.when(userRoleRepository.existsByIdRole(UserSecurityRole.ADMIN)).thenReturn(true); + } + securityService = new SecurityService(projectUserMemberRepository, projectGroupMemberRepository, userRepository, userRoleRepository, groupMemberRepository); + TestUtil.setField(securityService, "newUserRole", newUserRole); + TestUtil.setField(securityService, "firstAdminName", firstAdminName); + } + + @Test + void getProjectMemberRolesShouldReturnEmptySetWhenUserDoesntHaveAnyRole() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + Set projectMemberRoles = securityService.getProjectMemberRoles(null, null); + Assertions.assertNotNull(projectMemberRoles); + Assertions.assertEquals(0, projectMemberRoles.size()); + } + + @Test + void getProjectMemberRolesShouldReturnSetContainingOnlyRoleLinkedToUserWhenUserHasNoRoleLinkedToItsGroup() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + ProjectUserMember projectUserMember = new ProjectUserMember(); + projectUserMember.setRole(MemberRole.MAINTAINER); + Mockito.when(projectUserMemberRepository.findByProjectCodeAndIdMemberName("a", "b")).thenReturn(projectUserMember); + Set projectMemberRoles = securityService.getProjectMemberRoles("a", "b"); + Assertions.assertEquals(1, projectMemberRoles.size()); + Assertions.assertEquals(projectUserMember.getRole(), projectMemberRoles.iterator().next()); + } + + @Test + void getProjectMemberRolesShouldReturnSetContainingOnlyRoleLinkedToItsGroupWhenUserHasNoRoleLinkedToIt() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + ProjectGroupMember projectGroupMember = new ProjectGroupMember(); + projectGroupMember.setRole(MemberRole.MAINTAINER); + Mockito.when(projectGroupMemberRepository.findAllProjectGroupMemberByProjectCodeAndUserName("a", "b")).thenReturn(List.of(projectGroupMember)); + Set projectMemberRoles = securityService.getProjectMemberRoles("a", "b"); + Assertions.assertEquals(1, projectMemberRoles.size()); + Assertions.assertEquals(projectGroupMember.getRole(), projectMemberRoles.iterator().next()); + } + + @Test + void getProjectMemberRolesShouldReturnSetContainingAllRoleWhenUserHasRoleLinkedToItAndToItsGroup() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + ProjectUserMember projectUserMember = new ProjectUserMember(); + projectUserMember.setRole(MemberRole.MEMBER); + Mockito.when(projectUserMemberRepository.findByProjectCodeAndIdMemberName("a", "b")).thenReturn(projectUserMember); + ProjectGroupMember projectGroupMember = new ProjectGroupMember(); + projectGroupMember.setRole(MemberRole.MAINTAINER); + Mockito.when(projectGroupMemberRepository.findAllProjectGroupMemberByProjectCodeAndUserName("a", "b")).thenReturn(List.of(projectGroupMember)); + Set projectMemberRoles = securityService.getProjectMemberRoles("a", "b"); + Assertions.assertEquals(2, projectMemberRoles.size()); + Assertions.assertTrue(projectMemberRoles.contains(projectUserMember.getRole())); + Assertions.assertTrue(projectMemberRoles.contains(projectGroupMember.getRole())); + } + + @Test + void getGroupMemberRolesShouldReturnEmptySetWhenUserDoesntHaveAnyRole() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + Set groupMemberRoles = securityService.getGroupMemberRoles(null, null); + Assertions.assertNotNull(groupMemberRoles); + Assertions.assertEquals(0, groupMemberRoles.size()); + } + + @Test + void getGroupMemberRolesShouldReturnSetContainingOnlyUserGroupMemberRoleWhenItHasOne() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + GroupMember groupMember = new GroupMember(); + groupMember.setRole(MemberRole.MAINTAINER); + Mockito.when(groupMemberRepository.findByContainerIdentifierAndMemberName("a", "b")).thenReturn(groupMember); + Set groupMemberRoles = securityService.getGroupMemberRoles("a", "b"); + Assertions.assertEquals(1, groupMemberRoles.size()); + Assertions.assertEquals(groupMember.getRole(), groupMemberRoles.iterator().next()); + } + + @Test + void getUserRolesShouldReturnEmptySetWhenUserDoesntHaveAnyRole() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + List userRoles = securityService.getUserRoles(null); + Assertions.assertNotNull(userRoles); + Assertions.assertEquals(0, userRoles.size()); + } + + @Test + void getUserRolesShouldReturnSetContainingAllUserRoleWhenItHasAny() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + UserRole role1 = new UserRole(new User(), UserSecurityRole.PROJECT_OR_GROUP_CREATOR); + UserRole role2 = new UserRole(new User(), UserSecurityRole.AUDITING); + Mockito.when(userRoleRepository.findAllByIdUserId("a")).thenReturn(List.of(role1, role2)); + List userRoles = securityService.getUserRoles("a"); + Assertions.assertEquals(2, userRoles.size()); + Assertions.assertEquals(role1.getRole(), userRoles.get(0)); + Assertions.assertEquals(role2.getRole(), userRoles.get(1)); + } + + @Test + void initUserShouldDoNothingAndReturnUserWhenItAlreadyExists() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + User user = new User(); + Mockito.when(userRepository.findByNameAndIssuer("a", "b")).thenReturn(user); + User result = securityService.initUser("a", "b"); + Assertions.assertEquals(user, result); + Mockito.verify(userRepository, Mockito.times(0)).save(Mockito.any()); + Mockito.verify(userRoleRepository, Mockito.times(0)).save(Mockito.any()); + boolean adminCreated = TestUtil.getField(securityService, "adminCreated"); + Assertions.assertFalse(adminCreated); + } + + private static Stream provideUserCreationTestArguments() { + return Stream.of( + Arguments.of((String)null), + Arguments.of(""), + Arguments.of(" ")); + } + + @ParameterizedTest + @MethodSource("provideUserCreationTestArguments") + void initUserShouldCreateUserWithAdminAndNewUserRolesAndReturnUserWhenUserNotExistsAndAdminNotAlreadyCreatedAndFirstAdminNameIs(String fistAdminNameProperty) { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, fistAdminNameProperty); + User result = securityService.initUser("a", "b"); + Mockito.verify(userRepository).save(result); + Assertions.assertEquals("a", result.getName()); + Assertions.assertEquals("b", result.getIssuer()); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(UserRole.class); + Mockito.verify(userRoleRepository, Mockito.times(2)).save(roleCaptor.capture()); + List allRoles = roleCaptor.getAllValues(); + Assertions.assertEquals(2, allRoles.size()); + Assertions.assertEquals(UserSecurityRole.ADMIN, allRoles.get(0).getRole()); + Assertions.assertEquals(UserSecurityRole.PROJECT_OR_GROUP_CREATOR, allRoles.get(1).getRole()); + boolean adminCreated = TestUtil.getField(securityService, "adminCreated"); + Assertions.assertTrue(adminCreated); + } + + @Test + void initUserShouldCreateUserWithOnlyNewUserRolesAndReturnUserWhenUserNotExistsAndAdminNotAlreadyCreatedAndFirstAdminNameIsDefineToAnotherUserName() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, "c"); + User result = securityService.initUser("a", "b"); + Mockito.verify(userRepository).save(result); + Assertions.assertEquals("a", result.getName()); + Assertions.assertEquals("b", result.getIssuer()); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(UserRole.class); + Mockito.verify(userRoleRepository).save(roleCaptor.capture()); + List allRoles = roleCaptor.getAllValues(); + Assertions.assertEquals(1, allRoles.size()); + Assertions.assertEquals(UserSecurityRole.PROJECT_OR_GROUP_CREATOR, allRoles.get(0).getRole()); + boolean adminCreated = TestUtil.getField(securityService, "adminCreated"); + Assertions.assertFalse(adminCreated); + } + + @Test + void initUserShouldCreateUserWithAdminAndNewUserRolesAndReturnUserWhenUserNotExistsAndAdminNotAlreadyCreatedAndFirstAdminNameIsDefineToCurrentUserName() { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, "a"); + User result = securityService.initUser("a", "b"); + Mockito.verify(userRepository).save(result); + Assertions.assertEquals("a", result.getName()); + Assertions.assertEquals("b", result.getIssuer()); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(UserRole.class); + Mockito.verify(userRoleRepository, Mockito.times(2)).save(roleCaptor.capture()); + List allRoles = roleCaptor.getAllValues(); + Assertions.assertEquals(2, allRoles.size()); + Assertions.assertEquals(UserSecurityRole.ADMIN, allRoles.get(0).getRole()); + Assertions.assertEquals(UserSecurityRole.PROJECT_OR_GROUP_CREATOR, allRoles.get(1).getRole()); + boolean adminCreated = TestUtil.getField(securityService, "adminCreated"); + Assertions.assertTrue(adminCreated); + } + + @Test + void initUserShouldCreateUserWithNewUserRoleAndReturnUserWhenUserNotExistsAndAdminAlreadyCreated() { + prepareTest(true, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + User result = securityService.initUser("a", "b"); + Mockito.verify(userRepository).save(result); + Assertions.assertEquals("a", result.getName()); + Assertions.assertEquals("b", result.getIssuer()); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(UserRole.class); + Mockito.verify(userRoleRepository).save(roleCaptor.capture()); + List allRoles = roleCaptor.getAllValues(); + Assertions.assertEquals(1, allRoles.size()); + Assertions.assertEquals(UserSecurityRole.PROJECT_OR_GROUP_CREATOR, allRoles.get(0).getRole()); + } + + @Test + void initUserShouldNotCreateMultipleUserWithAdminRoleWhenMultipleUserInitAtTheSameTimeAndAdminNotAlreadyCreated() throws InterruptedException, ExecutionException { + prepareTest(false, UserSecurityRole.PROJECT_OR_GROUP_CREATOR, null); + CountDownLatch waiter = new CountDownLatch(1); + CountDownLatch waiter2 = new CountDownLatch(2); + Mockito.when(userRepository.save(Mockito.any())).thenAnswer(invocationOnMock -> { + waiter2.countDown(); + waiter.await(); + return null; + }); + ExecutorService executor = Executors.newFixedThreadPool(2); + Future firstUserInitResultFuture = executor.submit(() -> securityService.initUser("a", "b")); + Future secondUserInitResultFuture = executor.submit(() -> securityService.initUser("c", "d")); + executor.shutdown(); + waiter2.await(); + waiter.countDown(); + User firstUserInitResult = firstUserInitResultFuture.get(); + User secondUserInitResult = secondUserInitResultFuture.get(); + Mockito.verify(userRepository).save(firstUserInitResult); + Mockito.verify(userRepository).save(secondUserInitResult); + Assertions.assertEquals("a", firstUserInitResult.getName()); + Assertions.assertEquals("b", firstUserInitResult.getIssuer()); + Assertions.assertEquals("c", secondUserInitResult.getName()); + Assertions.assertEquals("d", secondUserInitResult.getIssuer()); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(UserRole.class); + Mockito.verify(userRoleRepository, Mockito.times(3)).save(roleCaptor.capture()); + List allRoles = roleCaptor.getAllValues(); + Assertions.assertEquals(3, allRoles.size()); + Assertions.assertEquals(UserSecurityRole.ADMIN, allRoles.get(0).getRole()); + Assertions.assertEquals(UserSecurityRole.PROJECT_OR_GROUP_CREATOR, allRoles.get(1).getRole()); + Assertions.assertEquals(UserSecurityRole.PROJECT_OR_GROUP_CREATOR, allRoles.get(2).getRole()); + } + + @Test + void initUserShouldCreateUserWithOnlyAdminRoleWhenAdminNotAlreadyCreatedAndNewUserRoleIsNull() { + prepareTest(false, null, null); + User result = securityService.initUser("a", "b"); + Mockito.verify(userRepository).save(result); + Assertions.assertEquals("a", result.getName()); + Assertions.assertEquals("b", result.getIssuer()); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(UserRole.class); + Mockito.verify(userRoleRepository).save(roleCaptor.capture()); + List allRoles = roleCaptor.getAllValues(); + Assertions.assertEquals(1, allRoles.size()); + Assertions.assertEquals(UserSecurityRole.ADMIN, allRoles.get(0).getRole()); + } + + @Test + void initUserShouldCreateUserWithoutRoleWhenAdminAlreadyCreatedAndNewUserRoleIsNull() { + prepareTest(true, null, null); + User result = securityService.initUser("a", "b"); + Mockito.verify(userRepository).save(result); + Assertions.assertEquals("a", result.getName()); + Assertions.assertEquals("b", result.getIssuer()); + Mockito.verify(userRoleRepository, Mockito.times(0)).save(Mockito.any()); + } +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/util/TestUtil.java b/code/api/api/src/test/java/com/decathlon/ara/util/TestUtil.java index e9cd227bd..354aa72de 100644 --- a/code/api/api/src/test/java/com/decathlon/ara/util/TestUtil.java +++ b/code/api/api/src/test/java/com/decathlon/ara/util/TestUtil.java @@ -133,7 +133,7 @@ public static T getField(Object o, String fieldName) { public static T getField(Object o, Class fieldClass, String fieldName) { Field declaredField; try { - declaredField = o.getClass().getDeclaredField(fieldName); + declaredField = fieldClass.getDeclaredField(fieldName); declaredField.setAccessible(true); return (T) declaredField.get(o); } catch (NoSuchFieldException e) { diff --git a/code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupMemberResourceTest.java b/code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupMemberResourceTest.java new file mode 100644 index 000000000..fe130516d --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupMemberResourceTest.java @@ -0,0 +1,41 @@ +package com.decathlon.ara.web.rest; + +import java.util.Map; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.User; +import com.decathlon.ara.service.GroupMemberService; +import com.decathlon.ara.service.MemberService; + +@ExtendWith(MockitoExtension.class) +public class GroupMemberResourceTest extends MemberResourceTest{ + + @Mock + private GroupMemberService groupMemberService; + + @InjectMocks + private GroupMemberResource groupMemberResource; + + @Override + MemberService getMemberService() { + return groupMemberService; + } + + @Override + MemberResource getMemberResource() { + return groupMemberResource; + } + + @Override + Map getParametersMap() { + return Map.of("groupName", "identifier"); + } + + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupResourceTest.java b/code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupResourceTest.java new file mode 100644 index 000000000..1dfed6224 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/web/rest/GroupResourceTest.java @@ -0,0 +1,79 @@ +package com.decathlon.ara.web.rest; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; + +import com.decathlon.ara.service.GroupService; +import com.decathlon.ara.service.dto.group.GroupDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; +import com.decathlon.ara.service.exception.NotUniqueException; + +@ExtendWith(MockitoExtension.class) +class GroupResourceTest { + + @Mock + private GroupService groupService; + + @InjectMocks + private GroupResource groupResource; + + @Test + void getAllShouldReturnServiceResult() { + List serviceResult = List.of(new GroupDTO()); + Mockito.when(groupService.findAll()).thenReturn(serviceResult); + List result = groupResource.getAll(); + Assertions.assertEquals(serviceResult, result); + } + + @Test + void getShouldThrowExceptionWhenServiceThrowAnException() throws NotFoundException { + Mockito.when(groupService.findOne("groupName")).thenThrow(new NotFoundException(null, null)); + Assertions.assertThrows(NotFoundException.class, () -> groupResource.get("groupName")); + } + + @Test + void getShouldReturnServiceResultWhenServiceSucceed() throws NotFoundException { + GroupDTO groupDTO = new GroupDTO(); + Mockito.when(groupService.findOne("groupName")).thenReturn(groupDTO); + GroupDTO result = groupResource.get("groupName"); + Assertions.assertEquals(groupDTO, result); + } + + @Test + void createShouldThrowExceptionWhenServiceThrowAnException() throws NotUniqueException { + GroupDTO groupDTO = new GroupDTO(); + Mockito.when(groupService.create(groupDTO)).thenThrow(new NotUniqueException(null, null, null, (String)null)); + Assertions.assertThrows(NotUniqueException.class, () -> groupResource.create(groupDTO)); + } + + @Test + void createShouldReturnEntityResponseCreatedWithServiceResultAsBodyWhenServiceSucceed() throws NotUniqueException { + GroupDTO groupDTO = new GroupDTO(); + Mockito.when(groupService.create(groupDTO)).thenReturn(groupDTO); + ResponseEntity response = groupResource.create(groupDTO); + Assertions.assertEquals(201, response.getStatusCodeValue()); + Assertions.assertEquals(groupDTO, response.getBody()); + } + + @Test + void deleteShouldThrowExceptionWhenServiceThrowAnException() throws BadRequestException { + Mockito.doThrow(new BadRequestException(null, null, null)).when(groupService).delete("groupName"); + Assertions.assertThrows(BadRequestException.class, () -> groupResource.delete("groupName")); + } + + @Test + void deleteShouldSucceedWhenServiceSucceed() throws BadRequestException { + Mockito.doNothing().when(groupService).delete("groupName"); + Assertions.assertDoesNotThrow(() ->groupResource.delete("groupName")); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/web/rest/MemberResourceTest.java b/code/api/api/src/test/java/com/decathlon/ara/web/rest/MemberResourceTest.java new file mode 100644 index 000000000..620d9a1be --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/web/rest/MemberResourceTest.java @@ -0,0 +1,126 @@ +package com.decathlon.ara.web.rest; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; + +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.MemberRelationship; +import com.decathlon.ara.domain.enumeration.MemberRole; +import com.decathlon.ara.service.MemberService; +import com.decathlon.ara.service.dto.member.MemberDTO; +import com.decathlon.ara.service.exception.BadRequestException; +import com.decathlon.ara.service.exception.NotFoundException; + +abstract class MemberResourceTest> { + + abstract MemberService getMemberService(); + abstract MemberResource getMemberResource(); + abstract Map getParametersMap(); + + @Test + void getAllShouldReturnServiceResult() { + List serviceResult = List.of(new MemberDTO()); + Mockito.when(getMemberService().findAll("identifier")).thenReturn(serviceResult); + List result = getMemberResource().getAll(getParametersMap()); + Assertions.assertEquals(serviceResult, result); + } + + @Test + void getShouldThrowExceptionWhenServiceThrowAnException() throws NotFoundException { + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + Mockito.when(getMemberService().findOne(identifier, "memberName")).thenThrow(new NotFoundException(null, null)); + Assertions.assertThrows(NotFoundException.class, () -> getMemberResource().get(parametersMap, "memberName")); + } + + @Test + void getShouldReturnServiceResultWhenServiceSucceed() throws NotFoundException { + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + MemberDTO serviceResult = new MemberDTO(); + Mockito.when(getMemberService().findOne(identifier, "memberName")).thenReturn(serviceResult); + MemberDTO result = getMemberResource().get(parametersMap, "memberName"); + Assertions.assertEquals(serviceResult, result); + } + + @Test + void addMemberShouldThrowExceptionWhenServiceThrowAnException() throws BadRequestException { + try (MockedStatic servletUriComponentsBuilderStaticMock = Mockito.mockStatic(ServletUriComponentsBuilder.class)){ + ServletUriComponentsBuilder servletUriComponentsBuilder = Mockito.mock(ServletUriComponentsBuilder.class, Mockito.RETURNS_SELF); + UriComponents uriComponents = Mockito.mock(UriComponents.class); + Mockito.when(uriComponents.toUri()).thenReturn(Mockito.mock(URI.class)); + Mockito.when(servletUriComponentsBuilder.build()).thenReturn(uriComponents); + servletUriComponentsBuilderStaticMock.when(ServletUriComponentsBuilder::fromCurrentRequestUri).thenReturn(servletUriComponentsBuilder); + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + MemberDTO memberDTO = new MemberDTO(); + Mockito.when(getMemberService().addMember(identifier, memberDTO)).thenThrow(new BadRequestException(null, null, null)); + Assertions.assertThrows(BadRequestException.class, () -> getMemberResource().addMember(parametersMap, memberDTO)); + } + } + + @Test + void addMemberShouldReturnResponseCreatedWithServiceResultAsBodyWhenServiceSucceed() throws BadRequestException { + try (MockedStatic servletUriComponentsBuilderStaticMock = Mockito.mockStatic(ServletUriComponentsBuilder.class)){ + ServletUriComponentsBuilder servletUriComponentsBuilder = Mockito.mock(ServletUriComponentsBuilder.class, Mockito.RETURNS_SELF); + UriComponents uriComponents = Mockito.mock(UriComponents.class); + Mockito.when(uriComponents.toUri()).thenReturn(Mockito.mock(URI.class)); + Mockito.when(servletUriComponentsBuilder.build()).thenReturn(uriComponents); + servletUriComponentsBuilderStaticMock.when(ServletUriComponentsBuilder::fromCurrentRequestUri).thenReturn(servletUriComponentsBuilder); + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + MemberDTO memberDTO = new MemberDTO(); + MemberDTO serviceResult = new MemberDTO(); + Mockito.when(getMemberService().addMember(identifier, memberDTO)).thenReturn(serviceResult); + ResponseEntity response = getMemberResource().addMember(parametersMap, memberDTO); + Assertions.assertEquals(201, response.getStatusCodeValue()); + Assertions.assertEquals(serviceResult, response.getBody()); + } + } + + @Test + void updateMemberRoleShouldThrowExceptionWhenServiceThrowAnException() throws BadRequestException { + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + MemberDTO memberDTO = new MemberDTO("memberName", MemberRole.ADMIN); + Mockito.when(getMemberService().updateMemberRole(identifier, memberDTO.getName(), memberDTO.getRole())).thenThrow(new BadRequestException(null, null, null)); + Assertions.assertThrows(BadRequestException.class, () -> getMemberResource().updateMemberRole(parametersMap, memberDTO.getName(), memberDTO)); + } + + @Test + void updateMemberRoleShouldReturnResponseCreatedWithServiceResultAsBodyWhenServiceSucceed() throws BadRequestException { + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + MemberDTO memberDTO = new MemberDTO("memberName", MemberRole.ADMIN); + MemberDTO serviceResult = new MemberDTO(); + Mockito.when(getMemberService().updateMemberRole(identifier, memberDTO.getName(), memberDTO.getRole())).thenReturn(serviceResult); + MemberDTO result = getMemberResource().updateMemberRole(parametersMap, memberDTO.getName(), memberDTO); + Assertions.assertEquals(serviceResult, result); + } + + @Test + void deleteMemberShouldThrowExceptionWhenServiceThrowAnException() throws BadRequestException { + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + Mockito.doThrow(new BadRequestException(null, null, null)).when(getMemberService()).deleteMember(identifier, "memberName"); + Assertions.assertThrows(BadRequestException.class, () -> getMemberResource().deleteMember(parametersMap, "memberName")); + } + + @Test + void deleteMemberShouldReturnResponseCreatedWithServiceResultAsBodyWhenServiceSucceed() throws BadRequestException { + Map parametersMap = getParametersMap(); + String identifier = getMemberResource().getIdentifier(parametersMap); + Mockito.doNothing().when(getMemberService()).deleteMember(identifier, "memberName"); + Assertions.assertDoesNotThrow(() -> getMemberResource().deleteMember(parametersMap, "memberName")); + } + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectGroupMemberResourceTest.java b/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectGroupMemberResourceTest.java new file mode 100644 index 000000000..6f66de6c1 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectGroupMemberResourceTest.java @@ -0,0 +1,41 @@ +package com.decathlon.ara.web.rest; + +import java.util.Map; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectGroupMember; +import com.decathlon.ara.service.MemberService; +import com.decathlon.ara.service.ProjectGroupMemberService; + +@ExtendWith(MockitoExtension.class) +public class ProjectGroupMemberResourceTest extends MemberResourceTest{ + + @Mock + private ProjectGroupMemberService projectGroupMemberService; + + @InjectMocks + private ProjectGroupMemberResource projectGroupMemberResource; + + @Override + MemberService getMemberService() { + return projectGroupMemberService; + } + + @Override + MemberResource getMemberResource() { + return projectGroupMemberResource; + } + + @Override + Map getParametersMap() { + return Map.of("projectCode", "identifier"); + } + + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectResourceIT.java b/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectResourceIT.java index c334c1524..ecb42144f 100644 --- a/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectResourceIT.java +++ b/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectResourceIT.java @@ -17,32 +17,30 @@ package com.decathlon.ara.web.rest; -import com.decathlon.ara.domain.Communication; -import com.decathlon.ara.domain.RootCause; -import com.decathlon.ara.repository.CommunicationRepository; -import com.decathlon.ara.repository.RootCauseRepository; -import com.decathlon.ara.service.dto.project.ProjectDTO; -import com.decathlon.ara.web.rest.util.HeaderUtil; -import com.github.springtestdbunit.DbUnitTestExecutionListener; -import com.github.springtestdbunit.annotation.DatabaseSetup; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; + import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; -import javax.persistence.EntityManager; -import javax.transaction.Transactional; -import java.util.List; - -import static com.decathlon.ara.util.TestUtil.NONEXISTENT; -import static com.decathlon.ara.util.TestUtil.header; -import static org.assertj.core.api.Assertions.assertThat; +import com.decathlon.ara.domain.Communication; +import com.decathlon.ara.domain.RootCause; +import com.decathlon.ara.repository.CommunicationRepository; +import com.decathlon.ara.repository.RootCauseRepository; +import com.decathlon.ara.service.dto.project.ProjectDTO; +import com.github.springtestdbunit.DbUnitTestExecutionListener; +import com.github.springtestdbunit.annotation.DatabaseSetup; @Disabled @SpringBootTest @@ -69,78 +67,6 @@ class ProjectResourceIT { @Autowired private EntityManager entityManager; - @Test - void create_ShouldInsertEntity_WhenAllRulesAreRespected() { - // GIVEN - final ProjectDTO project = new ProjectDTO(null, "new-code", "New name", false); - - // WHEN - ResponseEntity response = cut.create(project); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getHeaders().getLocation().getPath()).isEqualTo("/api/projects/" + response.getBody().getId()); - assertThat(response.getBody().getId()).isGreaterThan(3); - assertThat(response.getBody().getCode()).isEqualTo("new-code"); - assertThat(response.getBody().getName()).isEqualTo("New name"); - assertThat(response.getBody().isDefaultAtStartup()).isFalse(); - assertThat(cut.getAll()).containsExactly( // Ordered by name ASC - new ProjectDTO(response.getBody().getId(), "new-code", "New name", false), - new ProjectDTO(Long.valueOf(1), "project-y", "Project A", false), - new ProjectDTO(Long.valueOf(3), "project-z", "Project B", false), - new ProjectDTO(Long.valueOf(2), "project-x", "Project C", true)); - } - - @Test - void create_ShouldFailAsBadRequest_WhenIdProvided() { - // GIVEN - final ProjectDTO projectWithId = new ProjectDTO(NONEXISTENT, "is...", "...provided", false); - - // WHEN - ResponseEntity response = cut.create(projectWithId); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(header(response, HeaderUtil.ERROR)).isEqualTo("error.id_exists"); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("project"); - assertThat(header(response, HeaderUtil.MESSAGE)).isEqualTo("A new project cannot already have an ID."); - assertThatTableHasNotChangedInDataBase(); - } - - @Test - void create_ShouldFailAsNotUnique_WhenCodeAlreadyExists() { - // GIVEN - final ProjectDTO projectWithExistingCode = new ProjectDTO(null, "project-y", "any", false); - - // WHEN - ResponseEntity response = cut.create(projectWithExistingCode); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(header(response, HeaderUtil.ERROR)).isEqualTo("error.not_unique"); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("project"); - assertThat(header(response, HeaderUtil.MESSAGE)).isEqualTo("The code is already used by another project."); - assertThat(header(response, HeaderUtil.DUPLICATE_PROPERTY_NAME)).isEqualTo("code"); - assertThat(header(response, HeaderUtil.OTHER_ENTITY_KEY)).isEqualTo("1"); - assertThatTableHasNotChangedInDataBase(); - } - - @Test - void create_ShouldFailAsNotUnique_WhenNameAlreadyExists() { - // GIVEN - final ProjectDTO projectWithExistingName = new ProjectDTO(null, "new-code", "Project A", false); - - // WHEN - ResponseEntity response = cut.create(projectWithExistingName); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(header(response, HeaderUtil.ERROR)).isEqualTo("error.not_unique"); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("project"); - assertThat(header(response, HeaderUtil.MESSAGE)).isEqualTo("The name is already used by another project."); - assertThat(header(response, HeaderUtil.DUPLICATE_PROPERTY_NAME)).isEqualTo("name"); - assertThat(header(response, HeaderUtil.OTHER_ENTITY_KEY)).isEqualTo("1"); - assertThatTableHasNotChangedInDataBase(); - } - @Test void create_ShouldInsertCommunications_WhenCreatingAProject() { // GIVEN @@ -179,122 +105,18 @@ void create_ShouldInsertDefaultRootCauses_WhenCreatingAProject() { "Test to update"); } - @Test - void getAll_ShouldReturnAllEntitiesOrderedByName() { - // WHEN - List projects = cut.getAll(); - - // THEN - assertThat(projects).containsExactly( // Ordered by name ASC - new ProjectDTO(Long.valueOf(1), "project-y", "Project A", false), - new ProjectDTO(Long.valueOf(3), "project-z", "Project B", false), - new ProjectDTO(Long.valueOf(2), "project-x", "Project C", true)); - } - - @Test - void update_ShouldUpdateEntity_WhenAllRulesAreRespected() { - // GIVEN - final Long existingId = Long.valueOf(1); - final ProjectDTO project = new ProjectDTO(null, "renamed-code", "Renamed name", false); - - // WHEN - ResponseEntity response = cut.update(existingId, project); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(header(response, HeaderUtil.ALERT)).isEqualTo("ara.project.updated"); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("1"); - assertThat(cut.getAll()).containsExactly( // Ordered by name ASC - new ProjectDTO(Long.valueOf(3), "project-z", "Project B", false), - new ProjectDTO(Long.valueOf(2), "project-x", "Project C", true), - new ProjectDTO(Long.valueOf(1), "renamed-code", "Renamed name", false)); - } - - @Test - void update_ShouldNotFailAsNameNotUnique_WhenUpdatingWithoutAnyChange() { - // GIVEN - Long existingId = Long.valueOf(1); - final ProjectDTO project = new ProjectDTO(existingId, "project-y", "Project A", false); - - // WHEN - ResponseEntity response = cut.update(existingId, project); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(header(response, HeaderUtil.ALERT)).isEqualTo("ara.project.updated"); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("1"); - assertThatTableHasNotChangedInDataBase(); - } - - @Test - void update_ShouldFailAsNotFound_WhenUpdatingNonexistentEntity() { - // GIVEN - final ProjectDTO anyProject = new ProjectDTO(null, "Trying to...", "... update nonexistent", false); - - // WHEN - ResponseEntity response = cut.update(NONEXISTENT, anyProject); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(header(response, HeaderUtil.ERROR)).isEqualTo("error.not_found"); - assertThat(header(response, HeaderUtil.MESSAGE)).isEqualTo("The project does not exist: it has perhaps been removed."); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("project"); - assertThatTableHasNotChangedInDataBase(); - } - - @Test - void update_ShouldFailAsNotUnique_WhenCodeAlreadyExists() { - // GIVEN - final Long id = Long.valueOf(2); - final ProjectDTO projectWithExistingCode = new ProjectDTO(null, "project-y", "any", false); - - // WHEN - ResponseEntity response = cut.update(id, projectWithExistingCode); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(header(response, HeaderUtil.ERROR)).isEqualTo("error.not_unique"); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("project"); - assertThat(header(response, HeaderUtil.MESSAGE)).isEqualTo("The code is already used by another project."); - assertThat(header(response, HeaderUtil.DUPLICATE_PROPERTY_NAME)).isEqualTo("code"); - assertThat(header(response, HeaderUtil.OTHER_ENTITY_KEY)).isEqualTo("1"); - assertThatTableHasNotChangedInDataBase(); - } - - @Test - void update_ShouldFailAsNotUnique_WhenNameAlreadyExists() { - // GIVEN - final Long id = Long.valueOf(2); - final ProjectDTO projectWithExistingName = new ProjectDTO(null, "any", "Project A", false); - - // WHEN - ResponseEntity response = cut.update(id, projectWithExistingName); - - // THEN - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(header(response, HeaderUtil.ERROR)).isEqualTo("error.not_unique"); - assertThat(header(response, HeaderUtil.PARAMS)).isEqualTo("project"); - assertThat(header(response, HeaderUtil.MESSAGE)).isEqualTo("The name is already used by another project."); - assertThat(header(response, HeaderUtil.DUPLICATE_PROPERTY_NAME)).isEqualTo("name"); - assertThat(header(response, HeaderUtil.OTHER_ENTITY_KEY)).isEqualTo("1"); - assertThatTableHasNotChangedInDataBase(); - } - @Test void update_ShouldNotDeleteCommunications_WhenCalled() { // GIVEN - final Long id = Long.valueOf(1); + final String code = "project-y"; final ProjectDTO updatedProjectProperties = new ProjectDTO(null, "any", "any", false); // WHEN - cut.update(id, updatedProjectProperties); + cut.update(code, updatedProjectProperties); // THEN - assertThat(communicationRepository.findAllByProjectIdOrderByCode(id.longValue())).hasSize(1); + assertThat(communicationRepository.findAllByProjectIdOrderByCode(1l)).hasSize(1); } - private void assertThatTableHasNotChangedInDataBase() { - getAll_ShouldReturnAllEntitiesOrderedByName(); - } } diff --git a/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectUserMemberResourceTest.java b/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectUserMemberResourceTest.java new file mode 100644 index 000000000..2818e52c3 --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/web/rest/ProjectUserMemberResourceTest.java @@ -0,0 +1,41 @@ +package com.decathlon.ara.web.rest; + +import java.util.Map; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.decathlon.ara.domain.User; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.service.MemberService; +import com.decathlon.ara.service.ProjectUserMemberService; + +@ExtendWith(MockitoExtension.class) +public class ProjectUserMemberResourceTest extends MemberResourceTest{ + + @Mock + private ProjectUserMemberService projectUserMemberService; + + @InjectMocks + private ProjectUserMemberResource projectUserMemberResource; + + @Override + MemberService getMemberService() { + return projectUserMemberService; + } + + @Override + MemberResource getMemberResource() { + return projectUserMemberResource; + } + + @Override + Map getParametersMap() { + return Map.of("projectCode", "identifier"); + } + + +} diff --git a/code/api/api/src/test/java/com/decathlon/ara/web/rest/authentication/UserResourceTest.java b/code/api/api/src/test/java/com/decathlon/ara/web/rest/authentication/UserResourceTest.java new file mode 100644 index 000000000..2b291d92e --- /dev/null +++ b/code/api/api/src/test/java/com/decathlon/ara/web/rest/authentication/UserResourceTest.java @@ -0,0 +1,82 @@ +package com.decathlon.ara.web.rest.authentication; + +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import com.decathlon.ara.service.UserService; +import com.decathlon.ara.service.dto.user.UserDTO; +import com.decathlon.ara.service.exception.NotFoundException; + +@ExtendWith(MockitoExtension.class) +class UserResourceTest { + + @Mock + private OidcUser oidcUser; + + @Mock + private OAuth2User oauth2User; + + @Mock + private UserService userService; + + @InjectMocks + private UserResource userResource; + + @Test + void getAllShouldReturnServiceResult() { + List serviceResult = List.of(new UserDTO()); + Mockito.when(userService.findAll()).thenReturn(serviceResult); + List result = userResource.getAll(); + Assertions.assertEquals(serviceResult, result); + } + + @Test + void getUserDetailsShouldThrowExceptionWhenServiceThrowAnException() throws NotFoundException { + Mockito.when(userService.findOne("userName")).thenThrow(new NotFoundException(null, null)); + Assertions.assertThrows(NotFoundException.class, () -> userResource.getUserDetails("userName")); + } + + @Test + void getUserDetailsShouldReturnServiceResultWhenServiceSucceed() throws NotFoundException { + UserDTO userDTO = new UserDTO(); + Mockito.when(userService.findOne("userName")).thenReturn(userDTO); + UserDTO result = userResource.getUserDetails("userName"); + Assertions.assertEquals(userDTO, result); + } + + @Test + void whenOIDCUser_userDetails_should_not_be_null() { + when(oidcUser.getSubject()).thenReturn("oidc_id"); + var userDetails = userResource.getUserDetails(oidcUser); + Assertions.assertNotNull(userDetails); + Assertions.assertEquals("oidc_id", userDetails.getId()); + } + + @Test + void whenOAauth2User_userDetails_should_not_be_null() { + when(oauth2User.getAttribute("id")).thenReturn("oauth2_id"); + var userDetails = userResource.getUserDetails(oauth2User); + Assertions.assertNotNull(userDetails); + Assertions.assertEquals("oauth2_id", userDetails.getId()); + } + + @Test + void whenOAauth2UserAndOIDCUser_userDetails_should_be_generated_from_oidc() { + when(oidcUser.getFullName()).thenReturn("oidc_name"); + var userDetails = userResource.getUserDetails(oidcUser); + Assertions.assertNotNull(userDetails); + Assertions.assertEquals("oidc_name", userDetails.getName()); + } + +} diff --git a/code/api/api/src/test/resources/data.sql b/code/api/api/src/test/resources/data.sql index 26f8cf165..21d953822 100644 --- a/code/api/api/src/test/resources/data.sql +++ b/code/api/api/src/test/resources/data.sql @@ -15,8 +15,8 @@ * * ******************************************************************************/ -INSERT INTO `project` (`id`, `code`, `name`, `default_at_startup`) -VALUES (1,'the-demo-project','The Demo Project', 0); +INSERT INTO `project` (`id`, `code`, `name`) +VALUES (1,'the-demo-project','The Demo Project'); INSERT INTO `country` (`code`, `name`, `id`, `project_id`) VALUES diff --git a/code/api/api/src/test/resources/wiremock/oauth2/mappings/oauth2-mappings.json b/code/api/api/src/test/resources/wiremock/oauth2/mappings/oauth2-mappings.json new file mode 100644 index 000000000..cd04b5747 --- /dev/null +++ b/code/api/api/src/test/resources/wiremock/oauth2/mappings/oauth2-mappings.json @@ -0,0 +1,28 @@ +{ + "mappings": [ + { + "request":{ + "url":"/.well-known/openid-configuration", + "method":"GET" + }, + "response":{ + "status":200, + "body":'{"issuer":"{{request.baseUrl}}","authorization_endpoint":"{{request.baseUrl}}/oauth2/authorize","token_endpoint":"{{request.baseUrl}}/oauth2/token","token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"userinfo_endpoint":"{{request.baseUrl}}/oauth2/userinfo","jwks_uri":"{{request.baseUrl}}/oauth2/jwks","response_types_supported":["code"],"grant_types_supported":["authorization_code","client_credentials","refresh_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid"]}', + "headers":{ + "Content-Type":"application/json" + }, + "transformers": ["response-template"] + } + }, + { + "request":{ + "url":"/oauth2/token", + "method":"POST" + }, + "response":{ + "status":200, + "body":'' + } + } + ] +} \ No newline at end of file diff --git a/code/api/api/src/test/resources/wiremock/oidc/mappings/oidc-mappings.json b/code/api/api/src/test/resources/wiremock/oidc/mappings/oidc-mappings.json new file mode 100644 index 000000000..50bd0170d --- /dev/null +++ b/code/api/api/src/test/resources/wiremock/oidc/mappings/oidc-mappings.json @@ -0,0 +1,28 @@ +{ + "mappings": [ + { + "request":{ + "url":"/.well-known/openid-configuration", + "method":"GET" + }, + "response":{ + "status":200, + "body":'{"issuer":"{{request.baseUrl}}","authorization_endpoint":"{{request.baseUrl}}/oauth2/authorize","token_endpoint":"{{request.baseUrl}}/oauth2/token","token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"jwks_uri":"{{request.baseUrl}}/oauth2/jwks","response_types_supported":["code"],"grant_types_supported":["authorization_code","client_credentials","refresh_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid"]}', + "headers":{ + "Content-Type":"application/json" + }, + "transformers": ["response-template"] + } + }, + { + "request":{ + "url":"/oauth2/token", + "method":"POST" + }, + "response":{ + "status":200, + "body":'' + } + } + ] +} \ No newline at end of file diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/Group.java b/code/api/database/src/main/java/com/decathlon/ara/domain/Group.java new file mode 100644 index 000000000..9063a78cb --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/Group.java @@ -0,0 +1,34 @@ +package com.decathlon.ara.domain; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "group2") +public class Group implements Member { + + @Id + private String name; + + public Group() { + } + + public Group(String name) { + this.name = name; + } + + @Override + public String getMemberName() { + return getName(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/GroupMember.java b/code/api/database/src/main/java/com/decathlon/ara/domain/GroupMember.java new file mode 100644 index 000000000..9c406de63 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/GroupMember.java @@ -0,0 +1,100 @@ +package com.decathlon.ara.domain; + +import java.io.Serializable; +import java.util.Objects; + +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; + +import com.decathlon.ara.domain.enumeration.MemberRole; + +@Entity +public class GroupMember implements MemberRelationship { + + @Embeddable + public static class GroupMemberPk implements Serializable { + private static final long serialVersionUID = 1L; + private String groupName; + private String userName; + + public GroupMemberPk() { + } + + public GroupMemberPk(String groupName, String userName) { + this.groupName = groupName; + this.userName = userName; + } + } + + @EmbeddedId + private GroupMemberPk id; + + @MapsId("groupName") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "group_name") + private Group group; + + @MapsId("userName") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_name") + private User user; + + @Enumerated(EnumType.STRING) + private MemberRole role; + + public GroupMember() { + } + + public GroupMember(Group group, User user) { + Objects.requireNonNull(group); + Objects.requireNonNull(user); + this.id = new GroupMemberPk(group.getName(), user.getMemberName()); + this.group = group; + this.user = user; + } + + public Group getGroup() { + return group; + } + + public String getGroupName() { + return id.groupName; + } + + public User getUser() { + return user; + } + + @Override + public Group getContainer() { + return getGroup(); + } + + @Override + public User getMember() { + return getUser(); + } + + @Override + public String getMemberName() { + return id.userName; + } + + @Override + public MemberRole getRole() { + return role; + } + + @Override + public void setRole(MemberRole role) { + this.role = role; + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/Member.java b/code/api/database/src/main/java/com/decathlon/ara/domain/Member.java new file mode 100644 index 000000000..01224d0e5 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/Member.java @@ -0,0 +1,6 @@ +package com.decathlon.ara.domain; + +public interface Member { + + String getMemberName(); +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/MemberContainerRepository.java b/code/api/database/src/main/java/com/decathlon/ara/domain/MemberContainerRepository.java new file mode 100644 index 000000000..8a61691cd --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/MemberContainerRepository.java @@ -0,0 +1,11 @@ +package com.decathlon.ara.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.NoRepositoryBean; + +@NoRepositoryBean +public interface MemberContainerRepository extends JpaRepository { + + C findByContainerIdentifier(String identifier); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/MemberRelationship.java b/code/api/database/src/main/java/com/decathlon/ara/domain/MemberRelationship.java new file mode 100644 index 000000000..41678d52d --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/MemberRelationship.java @@ -0,0 +1,16 @@ +package com.decathlon.ara.domain; + +import com.decathlon.ara.domain.enumeration.MemberRole; + +public interface MemberRelationship { + + C getContainer(); + + M getMember(); + + String getMemberName(); + + MemberRole getRole(); + + void setRole(MemberRole role); +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/Project.java b/code/api/database/src/main/java/com/decathlon/ara/domain/Project.java index b1611a543..41e54b9ba 100644 --- a/code/api/database/src/main/java/com/decathlon/ara/domain/Project.java +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/Project.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import javax.persistence.CascadeType; import javax.persistence.Column; @@ -55,12 +56,6 @@ public Project(String code, String name) { this.name = name; } - /** - * True to use that project as the default one appearing at ARA's client startup when no project code is present in - * URL. Only one project can be declared as the default. - */ - private boolean defaultAtStartup; - @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "project", orphanRemoval = true) @OnDelete(action = OnDeleteAction.CASCADE) private List communications = new ArrayList<>(); @@ -73,6 +68,23 @@ public void addCommunication(Communication communication) { communications.add(communication); } + @Override + public int hashCode() { + return Objects.hash(code); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Project)) { + return false; + } + Project other = (Project) obj; + return Objects.equals(code, other.code); + } + public Long getId() { return id; } @@ -85,14 +97,6 @@ public String getName() { return name; } - public boolean isDefaultAtStartup() { - return defaultAtStartup; - } - - public void setDefaultAtStartup(boolean defaultAtStartup) { - this.defaultAtStartup = defaultAtStartup; - } - public List getCommunications() { return communications; } diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectGroupMember.java b/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectGroupMember.java new file mode 100644 index 000000000..fde3e80d2 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectGroupMember.java @@ -0,0 +1,18 @@ +package com.decathlon.ara.domain; + +import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; + +@Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +public class ProjectGroupMember extends ProjectMember { + + public ProjectGroupMember() { + } + + public ProjectGroupMember(Project project, Group group) { + super(project, group); + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectMember.java b/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectMember.java new file mode 100644 index 000000000..7ad986e79 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectMember.java @@ -0,0 +1,94 @@ +package com.decathlon.ara.domain; + +import java.io.Serializable; +import java.util.Objects; + +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MappedSuperclass; +import javax.persistence.MapsId; + +import com.decathlon.ara.domain.enumeration.MemberRole; + +@MappedSuperclass +public abstract class ProjectMember implements MemberRelationship { + + @Embeddable + public static class ProjectMemberPk implements Serializable { + private static final long serialVersionUID = 1L; + private Long projectId; + private String memberName; + + public ProjectMemberPk() { + } + + public ProjectMemberPk(Long projectId, String memberName) { + this.projectId = projectId; + this.memberName = memberName; + } + } + + @EmbeddedId + private ProjectMemberPk id; + + @MapsId("projectId") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "project_id") + private Project project; + + @MapsId("memberName") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "member_name") + private M member; + @Enumerated(EnumType.STRING) + private MemberRole role; + + protected ProjectMember() { + } + + protected ProjectMember(Project project, M member) { + Objects.requireNonNull(project); + Objects.requireNonNull(member); + this.id = new ProjectMemberPk(project.getId(), member.getMemberName()); + this.project = project; + this.member = member; + } + + public Project getProject() { + return project; + } + + public Long getProjectId() { + return id.projectId; + } + + @Override + public M getMember() { + return member; + } + + @Override + public Project getContainer() { + return getProject(); + } + + @Override + public String getMemberName() { + return id.memberName; + } + + @Override + public MemberRole getRole() { + return role; + } + + @Override + public void setRole(MemberRole role) { + this.role = role; + } +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectUserMember.java b/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectUserMember.java new file mode 100644 index 000000000..8a1d7d92b --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/ProjectUserMember.java @@ -0,0 +1,18 @@ +package com.decathlon.ara.domain; + +import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; + +@Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +public class ProjectUserMember extends ProjectMember { + + public ProjectUserMember() { + } + + public ProjectUserMember(Project project, User user) { + super(project, user); + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/User.java b/code/api/database/src/main/java/com/decathlon/ara/domain/User.java new file mode 100644 index 000000000..044d6fa1d --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/User.java @@ -0,0 +1,44 @@ +package com.decathlon.ara.domain; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +@Entity +@Table(uniqueConstraints = @UniqueConstraint(columnNames = { "name", "issuer" })) +public class User implements Member { + + @Id + private String id; + + private String name; + private String issuer; + + public User() { + } + + public User(String name, String issuer) { + this.id = name + "-" + issuer; + this.name = name; + this.issuer = issuer; + } + + @Override + public String getMemberName() { + return getId(); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getIssuer() { + return issuer; + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/UserPreference.java b/code/api/database/src/main/java/com/decathlon/ara/domain/UserPreference.java new file mode 100644 index 000000000..f1e36ca01 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/UserPreference.java @@ -0,0 +1,70 @@ +package com.decathlon.ara.domain; + +import java.io.Serializable; +import java.util.Objects; + +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; + +@Entity +public class UserPreference { + + @Embeddable + public static class UserPreferencePk implements Serializable { + private static final long serialVersionUID = 1L; + private String userId; + private String key; + + public UserPreferencePk() { + } + + public UserPreferencePk(String userId, String key) { + this.userId = userId; + this.key = key; + } + } + + @EmbeddedId + private UserPreferencePk id; + + @MapsId("userId") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private User user; + + private String value; + + public UserPreference() { + } + + public UserPreference(User user, String key) { + Objects.requireNonNull(user); + Objects.requireNonNull(key); + this.id = new UserPreferencePk(user.getId(), key); + this.user = user; + } + + public User getUser() { + return user; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getUserId() { + return id.userId; + } + + public String getKey() { + return id.key; + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/UserRole.java b/code/api/database/src/main/java/com/decathlon/ara/domain/UserRole.java new file mode 100644 index 000000000..141d9364b --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/UserRole.java @@ -0,0 +1,63 @@ +package com.decathlon.ara.domain; + +import java.io.Serializable; +import java.util.Objects; + +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; + +import com.decathlon.ara.domain.enumeration.UserSecurityRole; + +@Entity +public class UserRole { + @Embeddable + public static class UserRolePk implements Serializable { + private static final long serialVersionUID = 1L; + private String userId; + @Enumerated(EnumType.STRING) + private UserSecurityRole role; + + public UserRolePk() { + } + + public UserRolePk(String userId, UserSecurityRole role) { + this.userId = userId; + this.role = role; + } + } + + @EmbeddedId + private UserRolePk id; + + @MapsId("userId") + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private User user; + + public UserRole() { + } + + public UserRole(User user, UserSecurityRole role) { + Objects.requireNonNull(user); + Objects.requireNonNull(role); + this.id = new UserRolePk(user.getId(), role); + this.user = user; + } + + public User getUser() { + return user; + } + + public String getUserId() { + return id.userId; + } + + public UserSecurityRole getRole() { + return id.role; + } +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/MemberRole.java b/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/MemberRole.java new file mode 100644 index 000000000..7aedaa62d --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/MemberRole.java @@ -0,0 +1,27 @@ +package com.decathlon.ara.domain.enumeration; + +import java.util.HashSet; +import java.util.Set; + +public enum MemberRole { + + MEMBER(null, Permission.READ), + MAINTAINER(MEMBER, Permission.WRITE), + ADMIN(MAINTAINER, Permission.ADMIN); + + private Set permissions = new HashSet<>(); + + private MemberRole(MemberRole included, Permission... permissions) { + if (included != null) { + this.permissions.addAll(included.permissions); + } + for (Permission permission : permissions) { + this.permissions.add(permission); + } + } + + public boolean hasPermission(Permission permission) { + return permissions.contains(permission); + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/Permission.java b/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/Permission.java new file mode 100644 index 000000000..4abb68def --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/Permission.java @@ -0,0 +1,7 @@ +package com.decathlon.ara.domain.enumeration; + +public enum Permission { + + ADMIN, WRITE, READ; + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/UserSecurityRole.java b/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/UserSecurityRole.java new file mode 100644 index 000000000..f215f9ef8 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/domain/enumeration/UserSecurityRole.java @@ -0,0 +1,19 @@ +package com.decathlon.ara.domain.enumeration; + +public enum UserSecurityRole { + + ADMIN(null), + PROJECT_OR_GROUP_CREATOR(ADMIN), + AUDITING(null); + + private UserSecurityRole parent; + + private UserSecurityRole(UserSecurityRole parent) { + this.parent = parent; + } + + public UserSecurityRole getParent() { + return parent; + } + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/GroupMemberRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/GroupMemberRepository.java new file mode 100644 index 000000000..66843d33a --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/GroupMemberRepository.java @@ -0,0 +1,32 @@ +package com.decathlon.ara.repository; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.GroupMember; +import com.decathlon.ara.domain.User; + +@Repository +public interface GroupMemberRepository extends MemberRelationshipRepository { + + @Override + default List findAllByContainerIdentifier(String identifier) { + return findAllByIdGroupName(identifier); + } + + List findAllByIdGroupName(String groupName); + + List findAllByIdUserName(String memberName); + + @Override + default GroupMember findByContainerIdentifierAndMemberName(String identifier, String memberName) { + return findByIdGroupNameAndIdUserName(identifier, memberName); + } + + GroupMember findByIdGroupNameAndIdUserName(String groupName, String userName); + + void deleteByIdGroupName(String groupName); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/GroupRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/GroupRepository.java new file mode 100644 index 000000000..628286500 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/GroupRepository.java @@ -0,0 +1,23 @@ +package com.decathlon.ara.repository; + +import org.springframework.stereotype.Repository; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.MemberContainerRepository; + +@Repository +public interface GroupRepository extends MemberRepository, MemberContainerRepository { + + @Override + default Group findByContainerIdentifier(String identifier) { + return findByName(identifier); + } + + @Override + default Group findByMemberName(String memberName) { + return findByName(memberName); + } + + Group findByName(String name); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/MemberRelationshipRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/MemberRelationshipRepository.java new file mode 100644 index 000000000..00e834806 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/MemberRelationshipRepository.java @@ -0,0 +1,19 @@ +package com.decathlon.ara.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.NoRepositoryBean; + +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.MemberRelationship; +import com.decathlon.ara.domain.ProjectMember.ProjectMemberPk; + +@NoRepositoryBean +public interface MemberRelationshipRepository> extends JpaRepository { + + List findAllByContainerIdentifier(String identifier); + + R findByContainerIdentifierAndMemberName(String identifier, String memberName); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/MemberRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/MemberRepository.java new file mode 100644 index 000000000..d5ea65bab --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/MemberRepository.java @@ -0,0 +1,13 @@ +package com.decathlon.ara.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.NoRepositoryBean; + +import com.decathlon.ara.domain.Member; + +@NoRepositoryBean +public interface MemberRepository extends JpaRepository { + + M findByMemberName(String memberName); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectGroupMemberRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectGroupMemberRepository.java new file mode 100644 index 000000000..7754669c7 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectGroupMemberRepository.java @@ -0,0 +1,31 @@ +package com.decathlon.ara.repository; + +import java.util.List; +import java.util.Set; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.decathlon.ara.domain.Group; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectGroupMember; + +@Repository +public interface ProjectGroupMemberRepository extends ProjectMemberRepository { + + @Query(value = """ + select projectGroupMember from ProjectGroupMember projectGroupMember + join GroupMember groupMember on projectGroupMember.member.name = groupMember.group.name + where projectGroupMember.project.code = ?1 and groupMember.user.id = ?2 + """) + + List findAllProjectGroupMemberByProjectCodeAndUserName(String projectCode, String userName); + + @Query(value = """ + select distinct projectGroupMember.project from ProjectGroupMember projectGroupMember + join GroupMember groupMember on projectGroupMember.member.name = groupMember.group.name + where groupMember.user.id = ?1 + """) + Set findAllProjectByUserName(String userName); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectMemberRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectMemberRepository.java new file mode 100644 index 000000000..5317664fe --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectMemberRepository.java @@ -0,0 +1,32 @@ +package com.decathlon.ara.repository; + +import java.util.List; + +import org.springframework.data.repository.NoRepositoryBean; + +import com.decathlon.ara.domain.Member; +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectMember; + +@NoRepositoryBean +public interface ProjectMemberRepository> extends MemberRelationshipRepository { + + @Override + default java.util.List findAllByContainerIdentifier(String identifier) { + return findAllByProjectCode(identifier); + } + + List findAllByProjectCode(String projectCode); + + List findAllByIdMemberName(String memberName); + + @Override + default T findByContainerIdentifierAndMemberName(String identifier, String memberName) { + return findByProjectCodeAndIdMemberName(identifier, memberName); + } + + T findByProjectCodeAndIdMemberName(String projectCode, String memberName); + + void deleteByProjectCode(String code); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectRepository.java index 02c41c3a9..4676f6bd2 100644 --- a/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectRepository.java +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectRepository.java @@ -17,16 +17,23 @@ package com.decathlon.ara.repository; -import com.decathlon.ara.domain.Project; import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; + import org.springframework.stereotype.Repository; +import com.decathlon.ara.domain.MemberContainerRepository; +import com.decathlon.ara.domain.Project; + /** * Spring Data JPA repository for the Project entity. */ @Repository -public interface ProjectRepository extends JpaRepository { +public interface ProjectRepository extends MemberContainerRepository { + + @Override + default Project findByContainerIdentifier(String identifier) { + return findOneByCode(identifier); + } List findAllByOrderByName(); @@ -34,6 +41,4 @@ public interface ProjectRepository extends JpaRepository { Project findOneByName(String name); - Project findByDefaultAtStartup(boolean defaultAtStartup); - } diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectUserMemberRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectUserMemberRepository.java new file mode 100644 index 000000000..91af82b39 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/ProjectUserMemberRepository.java @@ -0,0 +1,22 @@ +package com.decathlon.ara.repository; + +import java.util.Set; + +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.decathlon.ara.domain.Project; +import com.decathlon.ara.domain.ProjectUserMember; +import com.decathlon.ara.domain.User; + +@Repository +public interface ProjectUserMemberRepository extends ProjectMemberRepository { + + @Query(value = """ + select distinct projectUserMember.project from ProjectUserMember projectUserMember + join projectUserMember.member user + where user.id = ?1 + """) + Set findAllProjectByUserName(String userName); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/UserPreferenceRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/UserPreferenceRepository.java new file mode 100644 index 000000000..ff59cb137 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/UserPreferenceRepository.java @@ -0,0 +1,14 @@ +package com.decathlon.ara.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.decathlon.ara.domain.UserPreference; +import com.decathlon.ara.domain.UserPreference.UserPreferencePk; + +@Repository +public interface UserPreferenceRepository extends JpaRepository { + + UserPreference findByIdUserIdAndIdKey(String id, String key); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/UserRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/UserRepository.java new file mode 100644 index 000000000..f84b73604 --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/UserRepository.java @@ -0,0 +1,17 @@ +package com.decathlon.ara.repository; + +import org.springframework.stereotype.Repository; + +import com.decathlon.ara.domain.User; + +@Repository +public interface UserRepository extends MemberRepository { + + @Override + default User findByMemberName(String memberName) { + return findById(memberName).orElse(null); + } + + User findByNameAndIssuer(String name, String issuer); + +} diff --git a/code/api/database/src/main/java/com/decathlon/ara/repository/UserRoleRepository.java b/code/api/database/src/main/java/com/decathlon/ara/repository/UserRoleRepository.java new file mode 100644 index 000000000..d994ceb0d --- /dev/null +++ b/code/api/database/src/main/java/com/decathlon/ara/repository/UserRoleRepository.java @@ -0,0 +1,19 @@ +package com.decathlon.ara.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.decathlon.ara.domain.UserRole; +import com.decathlon.ara.domain.UserRole.UserRolePk; +import com.decathlon.ara.domain.enumeration.UserSecurityRole; + +public interface UserRoleRepository extends JpaRepository { + + UserRole findByIdUserIdAndIdRole(String userId, UserSecurityRole role); + + List findAllByIdUserId(String userId); + + boolean existsByIdRole(UserSecurityRole role); + +} diff --git a/code/api/database/src/main/resources/db/changelog/changes/h2/20210301165639-h2_migration.yaml b/code/api/database/src/main/resources/db/changelog/changes/h2/20210301165639-h2_migration.yaml index 732a17851..4fa055948 100644 --- a/code/api/database/src/main/resources/db/changelog/changes/h2/20210301165639-h2_migration.yaml +++ b/code/api/database/src/main/resources/db/changelog/changes/h2/20210301165639-h2_migration.yaml @@ -1461,5 +1461,4 @@ databaseChangeLog: onUpdate: RESTRICT referencedColumnNames: ID referencedTableName: COUNTRY - validate: true - + validate: true \ No newline at end of file diff --git a/code/api/database/src/main/resources/db/changelog/changes/h2/20210618093532-change_id.yaml b/code/api/database/src/main/resources/db/changelog/changes/h2/20210618093532-change_id.yaml index cf6e79e5d..c3e8a773f 100644 --- a/code/api/database/src/main/resources/db/changelog/changes/h2/20210618093532-change_id.yaml +++ b/code/api/database/src/main/resources/db/changelog/changes/h2/20210618093532-change_id.yaml @@ -158,5 +158,4 @@ databaseChangeLog: - createSequence: sequenceName: TYPE_ID - sql: - sql: alter sequence TYPE_ID restart with (select max(id)+1 from TYPE) - + sql: alter sequence TYPE_ID restart with (select max(id)+1 from TYPE) \ No newline at end of file diff --git a/code/api/database/src/main/resources/db/changelog/changes/h2/20220504165317-rbac.yaml b/code/api/database/src/main/resources/db/changelog/changes/h2/20220504165317-rbac.yaml new file mode 100644 index 000000000..03ea05017 --- /dev/null +++ b/code/api/database/src/main/resources/db/changelog/changes/h2/20220504165317-rbac.yaml @@ -0,0 +1,368 @@ +databaseChangeLog: +- changeSet: + id: 1651676307273-1 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_7D + name: NAME + type: VARCHAR(255) + tableName: GROUP2 +- changeSet: + id: 1651676307273-2 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_10A + name: GROUP_NAME + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_10A + name: USER_NAME + type: VARCHAR(255) + - column: + name: ROLE + type: VARCHAR(255) + tableName: GROUP_MEMBER +- changeSet: + id: 1651676307273-3 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_3D + name: MEMBER_NAME + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_3D + name: PROJECT_ID + type: BIGINT + - column: + name: ROLE + type: VARCHAR(255) + tableName: PROJECT_GROUP_MEMBER +- changeSet: + id: 1651676307273-4 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_D + name: MEMBER_NAME + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_D + name: PROJECT_ID + type: BIGINT + - column: + name: ROLE + type: VARCHAR(255) + tableName: PROJECT_USER_MEMBER +- changeSet: + id: 1651676307273-5 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_27E + name: ID + type: VARCHAR(255) + - column: + name: ISSUER + type: VARCHAR(255) + - column: + name: NAME + type: VARCHAR(255) + tableName: USER +- changeSet: + id: 1651676307273-6 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_D7 + name: KEY + type: VARCHAR(255) + - column: + name: VALUE + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_D7 + name: USER_ID + type: VARCHAR(255) + tableName: USER_PREFERENCE +- changeSet: + id: 1651676307273-7 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_B + name: ROLE + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: CONSTRAINT_B + name: USER_ID + type: VARCHAR(255) + tableName: USER_ROLE +- changeSet: + id: 1651676307273-8 + author: '? (generated)' + changes: + - addUniqueConstraint: + columnNames: NAME, ISSUER + constraintName: UKIW5Y19ADPPAECW71IFXAIXQME + tableName: USER +- changeSet: + id: 1651676307273-9 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: PROJECT_ID + indexName: FK7DIM1HSOP1J0047BPWGH49QYN_INDEX_D + tableName: PROJECT_USER_MEMBER +- changeSet: + id: 1651676307273-10 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: USER_ID + indexName: FK859N2JVI8IVHUI0RL0ESWS6O_INDEX_B + tableName: USER_ROLE +- changeSet: + id: 1651676307273-11 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: GROUP_NAME + indexName: FKAUPDRFEY1Y1XHJ9QCTFVI4CI1_INDEX_1 + tableName: GROUP_MEMBER +- changeSet: + id: 1651676307273-12 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: PROJECT_ID + indexName: FKB8IHRBR19NTL33RL9F9QQPX2Y_INDEX_3 + tableName: PROJECT_GROUP_MEMBER +- changeSet: + id: 1651676307273-13 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: USER_ID + indexName: FKQ5OJ1CO3WU38LTB5G1XG9WEL4_INDEX_D + tableName: USER_PREFERENCE +- changeSet: + id: 1651676307273-14 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: MEMBER_NAME + indexName: FKQ6AIKCSND6SOYMO6AG0999JSI_INDEX_D + tableName: PROJECT_USER_MEMBER +- changeSet: + id: 1651676307273-15 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: USER_NAME + indexName: FKQ7YNVUVLA7MY5OQ5344Y93XKV_INDEX_1 + tableName: GROUP_MEMBER +- changeSet: + id: 1651676307273-16 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: MEMBER_NAME + indexName: FKRIP8UA9IUSV6YDD5H48YMG48Q_INDEX_3 + tableName: PROJECT_GROUP_MEMBER +- changeSet: + id: 1651676307273-17 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: PROJECT_ID + baseTableName: PROJECT_USER_MEMBER + constraintName: FK7DIM1HSOP1J0047BPWGH49QYN + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: ID + referencedTableName: PROJECT + validate: true +- changeSet: + id: 1651676307273-18 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: USER_ID + baseTableName: USER_ROLE + constraintName: FK859N2JVI8IVHUI0RL0ESWS6O + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: ID + referencedTableName: USER + validate: true +- changeSet: + id: 1651676307273-19 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: GROUP_NAME + baseTableName: GROUP_MEMBER + constraintName: FKAUPDRFEY1Y1XHJ9QCTFVI4CI1 + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: NAME + referencedTableName: GROUP2 + validate: true +- changeSet: + id: 1651676307273-20 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: PROJECT_ID + baseTableName: PROJECT_GROUP_MEMBER + constraintName: FKB8IHRBR19NTL33RL9F9QQPX2Y + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: ID + referencedTableName: PROJECT + validate: true +- changeSet: + id: 1651676307273-21 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: USER_ID + baseTableName: USER_PREFERENCE + constraintName: FKQ5OJ1CO3WU38LTB5G1XG9WEL4 + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: ID + referencedTableName: USER + validate: true +- changeSet: + id: 1651676307273-22 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: MEMBER_NAME + baseTableName: PROJECT_USER_MEMBER + constraintName: FKQ6AIKCSND6SOYMO6AG0999JSI + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: ID + referencedTableName: USER + validate: true +- changeSet: + id: 1651676307273-23 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: USER_NAME + baseTableName: GROUP_MEMBER + constraintName: FKQ7YNVUVLA7MY5OQ5344Y93XKV + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: ID + referencedTableName: USER + validate: true +- changeSet: + id: 1651676307273-24 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: MEMBER_NAME + baseTableName: PROJECT_GROUP_MEMBER + constraintName: FKRIP8UA9IUSV6YDD5H48YMG48Q + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: NAME + referencedTableName: GROUP2 + validate: true +- changeSet: + id: 1651676307273-25 + author: '? (generated)' + changes: + - dropColumn: + columnName: DEFAULT_AT_STARTUP + tableName: PROJECT + diff --git a/code/api/database/src/main/resources/db/changelog/changes/mysql/20220504170942-rbac.yaml b/code/api/database/src/main/resources/db/changelog/changes/mysql/20220504170942-rbac.yaml new file mode 100644 index 000000000..34d10aef3 --- /dev/null +++ b/code/api/database/src/main/resources/db/changelog/changes/mysql/20220504170942-rbac.yaml @@ -0,0 +1,326 @@ +databaseChangeLog: +- changeSet: + id: 1651677148230-1 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + name: name + type: VARCHAR(255) + tableName: group2 +- changeSet: + id: 1651677148230-2 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + name: group_name + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + name: user_name + type: VARCHAR(255) + - column: + name: role + type: VARCHAR(255) + tableName: group_member +- changeSet: + id: 1651677148230-3 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + name: member_name + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + name: project_id + type: BIGINT + - column: + name: role + type: VARCHAR(255) + tableName: project_group_member +- changeSet: + id: 1651677148230-4 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + name: member_name + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + name: project_id + type: BIGINT + - column: + name: role + type: VARCHAR(255) + tableName: project_user_member +- changeSet: + id: 1651677148230-5 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + name: id + type: VARCHAR(255) + - column: + name: issuer + type: VARCHAR(255) + - column: + name: name + type: VARCHAR(255) + tableName: user +- changeSet: + id: 1651677148230-6 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + name: key + type: VARCHAR(255) + - column: + name: value + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + name: user_id + type: VARCHAR(255) + tableName: user_preference +- changeSet: + id: 1651677148230-7 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + name: role + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + name: user_id + type: VARCHAR(255) + tableName: user_role +- changeSet: + id: 1651677148230-8 + author: '? (generated)' + changes: + - addUniqueConstraint: + columnNames: name, issuer + constraintName: UK7skqls7dr9dx4v3idddnkwj3l + tableName: user +- changeSet: + id: 1651677148230-9 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: project_id + indexName: FKachnfr2hedf28jyuvgda6ui9n + tableName: project_group_member +- changeSet: + id: 1651677148230-10 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: user_id + indexName: FKdk2lgoernfqwsmmb947osbnao + tableName: user_preference +- changeSet: + id: 1651677148230-11 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: user_id + indexName: FKhjx9nk20h4mo745tdqj8t8n9d + tableName: user_role +- changeSet: + id: 1651677148230-12 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: project_id + indexName: FKj5e6j34ckcn213qjiuxouuvgh + tableName: project_user_member +- changeSet: + id: 1651677148230-13 + author: '? (generated)' + changes: + - createIndex: + columns: + - column: + name: user_name + indexName: FKkpyi2sn2byahmyhq7oe2iebtx + tableName: group_member +- changeSet: + id: 1651677148230-14 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: member_name + baseTableName: project_group_member + constraintName: FK1s1jpajmrs7bjlffxqpn3gr4m + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: name + referencedTableName: group2 + validate: true +- changeSet: + id: 1651677148230-15 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: member_name + baseTableName: project_user_member + constraintName: FK8vs1kyv6ptmeuvlht6aigdi74 + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: user + validate: true +- changeSet: + id: 1651677148230-16 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: project_id + baseTableName: project_group_member + constraintName: FKachnfr2hedf28jyuvgda6ui9n + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: project + validate: true +- changeSet: + id: 1651677148230-17 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: user_id + baseTableName: user_preference + constraintName: FKdk2lgoernfqwsmmb947osbnao + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: user + validate: true +- changeSet: + id: 1651677148230-18 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: user_id + baseTableName: user_role + constraintName: FKhjx9nk20h4mo745tdqj8t8n9d + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: user + validate: true +- changeSet: + id: 1651677148230-19 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: project_id + baseTableName: project_user_member + constraintName: FKj5e6j34ckcn213qjiuxouuvgh + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: project + validate: true +- changeSet: + id: 1651677148230-20 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: user_name + baseTableName: group_member + constraintName: FKkpyi2sn2byahmyhq7oe2iebtx + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: id + referencedTableName: user + validate: true +- changeSet: + id: 1651677148230-21 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: group_name + baseTableName: group_member + constraintName: FKr5yhpeh4smwys5ckogokj87x3 + deferrable: false + initiallyDeferred: false + onDelete: RESTRICT + onUpdate: RESTRICT + referencedColumnNames: name + referencedTableName: group2 + validate: true +- changeSet: + id: 1651677148230-22 + author: '? (generated)' + changes: + - dropColumn: + columnName: default_at_startup + tableName: project + diff --git a/code/api/database/src/main/resources/db/changelog/changes/postgresql/20220504170125-rbac.yaml b/code/api/database/src/main/resources/db/changelog/changes/postgresql/20220504170125-rbac.yaml new file mode 100644 index 000000000..0d3b92190 --- /dev/null +++ b/code/api/database/src/main/resources/db/changelog/changes/postgresql/20220504170125-rbac.yaml @@ -0,0 +1,200 @@ +databaseChangeLog: +- changeSet: + id: 1651676613100-1 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: project_user_member_pkey + name: member_name + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: project_user_member_pkey + name: project_id + type: BIGINT + - column: + name: role + type: VARCHAR(255) + tableName: project_user_member +- changeSet: + id: 1651676613100-2 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: group2_pkey + name: name + type: VARCHAR(255) + tableName: group2 +- changeSet: + id: 1651676613100-3 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: group_member_pkey + name: group_name + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: group_member_pkey + name: user_name + type: VARCHAR(255) + - column: + name: role + type: VARCHAR(255) + tableName: group_member +- changeSet: + id: 1651676613100-4 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: project_group_member_pkey + name: member_name + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: project_group_member_pkey + name: project_id + type: BIGINT + - column: + name: role + type: VARCHAR(255) + tableName: project_group_member +- changeSet: + id: 1651676613100-5 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: project_id + baseTableName: project_user_member + constraintName: fk7dim1hsop1j0047bpwgh49qyn + deferrable: false + initiallyDeferred: false + onDelete: NO ACTION + onUpdate: NO ACTION + referencedColumnNames: id + referencedTableName: project + validate: true +- changeSet: + id: 1651676613100-6 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: project_id + baseTableName: project_group_member + constraintName: fkb8ihrbr19ntl33rl9f9qqpx2y + deferrable: false + initiallyDeferred: false + onDelete: NO ACTION + onUpdate: NO ACTION + referencedColumnNames: id + referencedTableName: project + validate: true +- changeSet: + id: 1651676613100-7 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: group_name + baseTableName: group_member + constraintName: fkaupdrfey1y1xhj9qctfvi4ci1 + deferrable: false + initiallyDeferred: false + onDelete: NO ACTION + onUpdate: NO ACTION + referencedColumnNames: name + referencedTableName: group2 + validate: true +- changeSet: + id: 1651676613100-8 + author: '? (generated)' + changes: + - addForeignKeyConstraint: + baseColumnNames: member_name + baseTableName: project_group_member + constraintName: fkrip8ua9iusv6ydd5h48ymg48q + deferrable: false + initiallyDeferred: false + onDelete: NO ACTION + onUpdate: NO ACTION + referencedColumnNames: name + referencedTableName: group2 + validate: true +- changeSet: + id: 1651676613100-9 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: user_preference_pkey + name: key + type: VARCHAR(255) + - column: + name: value + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: user_preference_pkey + name: user_id + type: VARCHAR(255) + tableName: user_preference +- changeSet: + id: 1651676613100-10 + author: '? (generated)' + changes: + - createTable: + columns: + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: user_role_pkey + name: role + type: VARCHAR(255) + - column: + constraints: + nullable: false + primaryKey: true + primaryKeyName: user_role_pkey + name: user_id + type: VARCHAR(255) + tableName: user_role +- changeSet: + id: 1651676613100-11 + author: '? (generated)' + changes: + - dropColumn: + columnName: default_at_startup + tableName: project + diff --git a/code/api/database/src/main/resources/db/changelog/db.changelog-master-h2.yaml b/code/api/database/src/main/resources/db/changelog/db.changelog-master-h2.yaml index dd187f93f..83fb38b82 100644 --- a/code/api/database/src/main/resources/db/changelog/db.changelog-master-h2.yaml +++ b/code/api/database/src/main/resources/db/changelog/db.changelog-master-h2.yaml @@ -13,3 +13,5 @@ databaseChangeLog: file: classpath*:db/changelog/changes/h2/20220126152954-missing_changes.yaml - include: file: classpath*:db/changelog/changes/h2/20220126171337-not_null_constraints.yaml + - include: + file: classpath*:db/changelog/changes/h2/20220504165317-rbac.yaml diff --git a/code/api/database/src/main/resources/db/changelog/db.changelog-master-mysql.yaml b/code/api/database/src/main/resources/db/changelog/db.changelog-master-mysql.yaml index 92fbd06b6..d19d01dab 100644 --- a/code/api/database/src/main/resources/db/changelog/db.changelog-master-mysql.yaml +++ b/code/api/database/src/main/resources/db/changelog/db.changelog-master-mysql.yaml @@ -17,3 +17,5 @@ databaseChangeLog: file: classpath*:db/changelog/changes/mysql/20220125173016-delete_cascade_on_problem_occurrences.yaml - include: file: classpath*:db/changelog/changes/mysql/20220126170954-not_null_constraints.yaml + - include: + file: classpath*:db/changelog/changes/mysql/20220504170942-rbac.yaml diff --git a/code/api/database/src/main/resources/db/changelog/db.changelog-master-postgresql.yaml b/code/api/database/src/main/resources/db/changelog/db.changelog-master-postgresql.yaml index 87bbce600..07c70be08 100644 --- a/code/api/database/src/main/resources/db/changelog/db.changelog-master-postgresql.yaml +++ b/code/api/database/src/main/resources/db/changelog/db.changelog-master-postgresql.yaml @@ -21,3 +21,5 @@ databaseChangeLog: file: classpath*:db/changelog/changes/postgresql/20220125164557-remove-unexpected-index.yaml - include: file: classpath*:db/changelog/changes/postgresql/20220318151922-not_null_constraints.yaml + - include: + file: classpath*:db/changelog/changes/postgresql/20220504170125-rbac.yaml diff --git a/code/api/database/src/main/resources/ehcache.xml b/code/api/database/src/main/resources/ehcache.xml index 13c5ba85d..2b7034d4e 100644 --- a/code/api/database/src/main/resources/ehcache.xml +++ b/code/api/database/src/main/resources/ehcache.xml @@ -52,7 +52,7 @@ timeToIdleSeconds="300" timeToLiveSeconds="3600"/> - + @@ -79,4 +79,23 @@ timeToIdleSeconds="300" timeToLiveSeconds="3600"/> + + + + + + + + + + + + + + + + + + + diff --git a/code/web-ui/src/libs/api.js b/code/web-ui/src/libs/api.js index 0d2afc329..af8883c6e 100644 --- a/code/web-ui/src/libs/api.js +++ b/code/web-ui/src/libs/api.js @@ -39,7 +39,7 @@ api.REQUEST_OPTIONS = { api.paths = { authenticationConfiguration: () => `${AUTH}/configuration`, loggedStatus: `${AUTH}/status`, - userDetails: `${API_PATH}/user/details`, + userDetails: `${API_PATH}/users/current/details`, communications: (viewOrProjectCode) => projectPath(viewOrProjectCode) + '/communications', countries: (viewOrProjectCode) => projectPath(viewOrProjectCode) + '/countries', cycleDefinitions: (viewOrProjectCode) => projectPath(viewOrProjectCode) + '/cycle-definitions', diff --git a/code/web-ui/src/views/management-projects.vue b/code/web-ui/src/views/management-projects.vue index a8ae0a1fb..9997f9b0d 100644 --- a/code/web-ui/src/views/management-projects.vue +++ b/code/web-ui/src/views/management-projects.vue @@ -72,12 +72,6 @@ justCreatedDemoProject: false, introduction: 'Projects are isolated areas in ARA to manage test reports of several applications or standalone components.', fields: [ - { - code: 'id', - type: 'hidden', - newValue: -1, - primaryKey: true - }, { code: 'code', name: 'Code', @@ -88,7 +82,8 @@ createOnlyBecause: 'the code ends-up in URLs of ARA, and people should be allowed to bookmark fixed URLs or copy/past them in other services (defect tracking system, wiki, etc.)', newValue: '', width: undefined, - help: 'The technical code of the project, to use in ARA URLs (as well as API URLs used by continuous integration to push data to ARA). Eg. "phoenix-front".' + help: 'The technical code of the project, to use in ARA URLs (as well as API URLs used by continuous integration to push data to ARA). Eg. "phoenix-front".', + primaryKey: true }, { code: 'name',