From 55af7538c3e530633b99e6af8842aa9cd2482f88 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka <martin.ledvinka@fel.cvut.cz> Date: Wed, 22 Nov 2023 15:41:23 +0100 Subject: [PATCH 1/7] [kbss-cvut/23ava-distribution#18] Implement user impersonate using Spring Security SwitchUserFilter. Also harmonize current user principal setting with how other Spring Security parts work (using UserDetails instead of direct username). --- .../kbss/study/config/SecurityConfig.java | 30 ++++++++- .../cvut/kbss/study/rest/UserController.java | 18 ------ .../security/CustomSwitchUserFilter.java | 19 ++++++ .../study/security/model/UserDetails.java | 10 ++- .../study/service/security/SecurityUtils.java | 15 ++--- src/main/resources/logback.xml | 6 ++ .../study/environment/util/Environment.java | 8 ++- .../kbss/study/rest/UserControllerTest.java | 40 ------------ .../security/CustomSwitchUserFilterTest.java | 61 +++++++++++++++++++ .../OntologyAuthenticationProviderTest.java | 2 +- 10 files changed, 136 insertions(+), 73 deletions(-) create mode 100644 src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java create mode 100644 src/test/java/cz/cvut/kbss/study/security/CustomSwitchUserFilterTest.java diff --git a/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java b/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java index 72ce871d..697b3196 100644 --- a/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java +++ b/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java @@ -2,8 +2,10 @@ import cz.cvut.kbss.study.exception.RecordManagerException; import cz.cvut.kbss.study.security.CsrfHeaderFilter; +import cz.cvut.kbss.study.security.CustomSwitchUserFilter; import cz.cvut.kbss.study.security.SecurityConstants; import cz.cvut.kbss.study.service.ConfigReader; +import cz.cvut.kbss.study.service.security.UserDetailsService; import cz.cvut.kbss.study.util.ConfigParam; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,10 +22,12 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -31,7 +35,11 @@ import java.net.MalformedURLException; import java.net.URL; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; @ConditionalOnProperty(prefix = "security", name = "provider", havingValue = "internal", matchIfMissing = true) @Configuration @@ -66,10 +74,13 @@ public SecurityConfig(AuthenticationFailureHandler authenticationFailureHandler, } @Bean - public SecurityFilterChain filterChain(HttpSecurity http, ConfigReader config) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http, ConfigReader config, + UserDetailsService userDetailsService) throws Exception { LOG.debug("Using internal security mechanisms."); final AuthenticationManager authManager = buildAuthenticationManager(http); - http.authorizeHttpRequests((auth) -> auth.anyRequest().permitAll()) + http.authorizeHttpRequests( + (auth) -> auth.requestMatchers("/rest/users/impersonate").hasRole(SecurityConstants.ROLE_ADMIN) + .anyRequest().permitAll()) .cors((auth) -> auth.configurationSource(corsConfigurationSource(config))) .csrf(AbstractHttpConfigurer::disable) .addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class) @@ -80,6 +91,8 @@ public SecurityFilterChain filterChain(HttpSecurity http, ConfigReader config) t .logout((auth) -> auth.logoutUrl(SecurityConstants.LOGOUT_URI) .logoutSuccessHandler(logoutSuccessHandler) .invalidateHttpSession(true).deleteCookies(COOKIES_TO_DESTROY)) + .sessionManagement((auth) -> auth.maximumSessions(1)) + .addFilterAfter(switchUserFilter(userDetailsService), AuthorizationFilter.class) .authenticationManager(authManager); return http.build(); } @@ -133,4 +146,15 @@ private static Optional<String> getApplicationUrlOrigin(ConfigReader configReade throw new RecordManagerException("Invalid configuration parameter " + ConfigParam.APP_CONTEXT + ".", e); } } + + @Bean + public SwitchUserFilter switchUserFilter(UserDetailsService userDetailsService) { + final SwitchUserFilter filter = new CustomSwitchUserFilter(); + filter.setUserDetailsService(userDetailsService); + filter.setUsernameParameter("username"); + filter.setSwitchUserUrl("/rest/users/impersonate"); + filter.setExitUserUrl("/rest/users/impersonate/logout"); + filter.setSuccessHandler(authenticationSuccessHandler); + return filter; + } } diff --git a/src/main/java/cz/cvut/kbss/study/rest/UserController.java b/src/main/java/cz/cvut/kbss/study/rest/UserController.java index 42fac83e..b0976217 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/UserController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/UserController.java @@ -3,14 +3,11 @@ import cz.cvut.kbss.study.exception.NotFoundException; import cz.cvut.kbss.study.model.Institution; import cz.cvut.kbss.study.model.User; -import cz.cvut.kbss.study.model.Vocabulary; import cz.cvut.kbss.study.rest.exception.BadRequestException; import cz.cvut.kbss.study.rest.util.RestUtils; import cz.cvut.kbss.study.security.SecurityConstants; -import cz.cvut.kbss.study.security.model.UserDetails; import cz.cvut.kbss.study.service.InstitutionService; import cz.cvut.kbss.study.service.UserService; -import cz.cvut.kbss.study.service.security.SecurityUtils; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -208,19 +205,4 @@ private User getByToken(String token) { assert token != null; return userService.findByToken(token); } - - @PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "')") - @PostMapping(value = "/impersonate", consumes = MediaType.TEXT_PLAIN_VALUE) - @ResponseStatus(HttpStatus.NO_CONTENT) - public void impersonate(@RequestBody String username) { - User user = getByUsername(username); - if (user.getTypes().contains(Vocabulary.s_c_administrator)) { - throw new BadRequestException("Cannot impersonate admin."); - } - UserDetails ud = new UserDetails(user); - SecurityUtils.setCurrentUser(ud); - if (LOG.isTraceEnabled()) { - LOG.trace("User {} impersonated.", user.getUsername()); - } - } } diff --git a/src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java b/src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java new file mode 100644 index 00000000..e8a51bcd --- /dev/null +++ b/src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java @@ -0,0 +1,19 @@ +package cz.cvut.kbss.study.security; + +import cz.cvut.kbss.study.rest.exception.BadRequestException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; + +public class CustomSwitchUserFilter extends SwitchUserFilter { + + @Override + protected Authentication attemptSwitchUser(HttpServletRequest request) throws AuthenticationException { + final Authentication switchTo = super.attemptSwitchUser(request); + if (switchTo.getAuthorities().stream().anyMatch(a -> SecurityConstants.ROLE_ADMIN.equals(a.getAuthority()))) { + throw new BadRequestException("Cannot impersonate admin."); + } + return switchTo; + } +} diff --git a/src/main/java/cz/cvut/kbss/study/security/model/UserDetails.java b/src/main/java/cz/cvut/kbss/study/security/model/UserDetails.java index 78eb1eac..90c882f5 100644 --- a/src/main/java/cz/cvut/kbss/study/security/model/UserDetails.java +++ b/src/main/java/cz/cvut/kbss/study/security/model/UserDetails.java @@ -65,7 +65,7 @@ public String getUsername() { @Override public boolean isAccountNonExpired() { - return false; + return true; } @Override @@ -86,4 +86,12 @@ public boolean isEnabled() { public User getUser() { return user; } + + @Override + public String toString() { + return "UserDetails{" + + "user=" + user + + ", authorities=" + authorities + + '}'; + } } diff --git a/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java b/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java index fd51a950..48fd9bf6 100644 --- a/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java +++ b/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java @@ -40,15 +40,15 @@ public SecurityUtils(UserDao userDao, PatientRecordDao patientRecordDao, ConfigR * Sets the current security context to the user represented by the provided user details. * <p> * Note that this method erases credentials from the provided user details for security reasons. - * + * <p> * This method should be used only when internal authentication is used. * * @param userDetails User details */ public static AbstractAuthenticationToken setCurrentUser(UserDetails userDetails) { - final UsernamePasswordAuthenticationToken - token = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), - userDetails.getAuthorities()); + final UsernamePasswordAuthenticationToken token = + UsernamePasswordAuthenticationToken.authenticated(userDetails, userDetails.getPassword(), + userDetails.getAuthorities()); token.setDetails(userDetails); token.eraseCredentials(); // Do not pass credentials around @@ -70,8 +70,8 @@ public User getCurrentUser() { if (principal instanceof Jwt) { return resolveAccountFromOAuthPrincipal((Jwt) principal); } else { - assert principal instanceof String; - final String username = context.getAuthentication().getPrincipal().toString(); + assert principal instanceof UserDetails; + final String username = context.getAuthentication().getName(); return userDao.findByUsername(username); } } @@ -81,7 +81,8 @@ private User resolveAccountFromOAuthPrincipal(Jwt principal) { final List<String> roles = new OidcGrantedAuthoritiesExtractor(config).extractRoles(principal); final User user = userDao.findByUsername(userInfo.getPreferredUsername()); if (user == null) { - throw new NotFoundException("User with username '" + userInfo.getPreferredUsername() + "' not found in repository."); + throw new NotFoundException( + "User with username '" + userInfo.getPreferredUsername() + "' not found in repository."); } roles.stream().map(Role::forName).filter(Optional::isPresent).forEach(r -> user.addType(r.get().getType())); return user; diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 997332de..4c13c81d 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -47,6 +47,12 @@ <appender-ref ref="LOGFILE"/> </logger> + <!-- Restrict logging of Spring bean customization --> + <logger name="cz.cvut.kbss.study.security.CustomSwitchUserFilter" level="INFO" additivity="false"> + <appender-ref ref="STDOUT"/> + <appender-ref ref="LOGFILE"/> + </logger> + <!-- Logger for our app --> <logger name="cz.cvut.kbss" level="TRACE" additivity="false"> <appender-ref ref="STDOUT"/> diff --git a/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java b/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java index d45af3da..e4b16fc3 100644 --- a/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java +++ b/src/test/java/cz/cvut/kbss/study/environment/util/Environment.java @@ -34,9 +34,11 @@ public static void setCurrentUser(User user) { currentUser = user; final UserDetails userDetails = new UserDetails(user); SecurityContext context = new SecurityContextImpl(); - context.setAuthentication( - new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), - userDetails.getAuthorities())); + final UsernamePasswordAuthenticationToken token = + UsernamePasswordAuthenticationToken.authenticated(userDetails, userDetails.getPassword(), + userDetails.getAuthorities()); + token.setDetails(userDetails); + context.setAuthentication(token); SecurityContextHolder.setContext(context); } diff --git a/src/test/java/cz/cvut/kbss/study/rest/UserControllerTest.java b/src/test/java/cz/cvut/kbss/study/rest/UserControllerTest.java index 5cad18ac..ba0159fe 100644 --- a/src/test/java/cz/cvut/kbss/study/rest/UserControllerTest.java +++ b/src/test/java/cz/cvut/kbss/study/rest/UserControllerTest.java @@ -6,7 +6,6 @@ import cz.cvut.kbss.study.environment.util.Environment; import cz.cvut.kbss.study.model.Institution; import cz.cvut.kbss.study.model.User; -import cz.cvut.kbss.study.model.Vocabulary; import cz.cvut.kbss.study.service.InstitutionService; import cz.cvut.kbss.study.service.UserService; import org.junit.jupiter.api.BeforeEach; @@ -22,10 +21,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -338,41 +335,4 @@ public void sendInvitationDeleteReturnsResponseStatusNoContent() throws Exceptio assertEquals(HttpStatus.NO_CONTENT, HttpStatus.valueOf(result.getResponse().getStatus())); verify(userServiceMock).findByUsername(username); } - - @Test - public void impersonateReturnsResponseStatusNoContent() throws Exception { - final String username = "tom"; - - when(userServiceMock.findByUsername(username)).thenReturn(Environment.getCurrentUser()); - - final MvcResult result = mockMvc.perform(post("/users/impersonate/") - .content(username) - .contentType(MediaType.TEXT_PLAIN_VALUE)) - .andReturn(); - - assertEquals(HttpStatus.NO_CONTENT, HttpStatus.valueOf(result.getResponse().getStatus())); - verify(userServiceMock).findByUsername(username); - } - - @Test - public void impersonateAdministratorReturnsResponseStatusBadRequest() throws Exception { - final String username = "tom"; - final Institution institution = Generator.generateInstitution(); - final User user = Generator.generateUser(institution); - - Set<String> types = new HashSet<>(); - types.add(Vocabulary.s_c_administrator); - types.add(Vocabulary.s_c_doctor); - user.setTypes(types); - - when(userServiceMock.findByUsername(username)).thenReturn(user); - - final MvcResult result = mockMvc.perform(post("/users/impersonate/") - .content(username) - .contentType(MediaType.TEXT_PLAIN_VALUE)) - .andReturn(); - - assertEquals(HttpStatus.BAD_REQUEST, HttpStatus.valueOf(result.getResponse().getStatus())); - verify(userServiceMock).findByUsername(username); - } } diff --git a/src/test/java/cz/cvut/kbss/study/security/CustomSwitchUserFilterTest.java b/src/test/java/cz/cvut/kbss/study/security/CustomSwitchUserFilterTest.java new file mode 100644 index 00000000..f7e2b4b4 --- /dev/null +++ b/src/test/java/cz/cvut/kbss/study/security/CustomSwitchUserFilterTest.java @@ -0,0 +1,61 @@ +package cz.cvut.kbss.study.security; + +import cz.cvut.kbss.study.environment.generator.Generator; +import cz.cvut.kbss.study.environment.util.Environment; +import cz.cvut.kbss.study.model.User; +import cz.cvut.kbss.study.model.Vocabulary; +import cz.cvut.kbss.study.rest.exception.BadRequestException; +import cz.cvut.kbss.study.security.model.UserDetails; +import cz.cvut.kbss.study.service.security.UserDetailsService; +import org.junit.jupiter.api.BeforeEach; +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.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.core.Authentication; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CustomSwitchUserFilterTest { + + @Mock + private UserDetailsService userDetailsService; + + private CustomSwitchUserFilter sut; + + @BeforeEach + void setUp() { + this.sut = new CustomSwitchUserFilter(); + sut.setUserDetailsService(userDetailsService); + } + + @Test + void attemptSwitchUserSwitchesCurrentUserToTarget() { + final User source = Generator.generateUser(null); + source.addType(Vocabulary.s_c_administrator); + Environment.setCurrentUser(source); + final User target = Generator.generateUser(null); + when(userDetailsService.loadUserByUsername(target.getUsername())).thenReturn(new UserDetails(target)); + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("username", target.getUsername()); + final Authentication result = sut.attemptSwitchUser(request); + assertEquals(target.getUsername(), result.getName()); + } + + @Test + void attemptSwitchUserThrowsBadRequestExceptionWhenTargetUserIsAdmin() { + final User source = Generator.generateUser(null); + source.addType(Vocabulary.s_c_administrator); + Environment.setCurrentUser(source); + final User target = Generator.generateUser(null); + target.addType(Vocabulary.s_c_administrator); + when(userDetailsService.loadUserByUsername(target.getUsername())).thenReturn(new UserDetails(target)); + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("username", target.getUsername()); + assertThrows(BadRequestException.class, () -> sut.attemptSwitchUser(request)); + } +} \ No newline at end of file diff --git a/src/test/java/cz/cvut/kbss/study/security/OntologyAuthenticationProviderTest.java b/src/test/java/cz/cvut/kbss/study/security/OntologyAuthenticationProviderTest.java index c2ece819..f71cd019 100644 --- a/src/test/java/cz/cvut/kbss/study/security/OntologyAuthenticationProviderTest.java +++ b/src/test/java/cz/cvut/kbss/study/security/OntologyAuthenticationProviderTest.java @@ -50,7 +50,7 @@ public void successfulAuthenticationSetsSecurityContext() { final Authentication result = provider.authenticate(auth); assertTrue(result.isAuthenticated()); assertNotNull(SecurityContextHolder.getContext()); - assertEquals(BaseServiceTestRunner.USERNAME, SecurityContextHolder.getContext().getAuthentication().getPrincipal()); + assertEquals(BaseServiceTestRunner.USERNAME, SecurityContextHolder.getContext().getAuthentication().getName()); } @Test From d538a6f792a1d86f463bb2c91eac40d3092360cf Mon Sep 17 00:00:00 2001 From: Martin Ledvinka <martin.ledvinka@fel.cvut.cz> Date: Wed, 22 Nov 2023 15:54:41 +0100 Subject: [PATCH 2/7] [kbss-cvut/23ava-distribution#18] Provide client with info that current user is impersonator. --- .../cz/cvut/kbss/study/config/SecurityConfig.java | 2 +- .../study/security/CustomSwitchUserFilter.java | 3 +++ .../study/service/security/SecurityUtils.java | 10 ++++++++-- src/main/resources/model.ttl | 4 ++++ .../study/service/security/SecurityUtilsTest.java | 15 +++++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java b/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java index 697b3196..fde5a182 100644 --- a/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java +++ b/src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java @@ -79,7 +79,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, ConfigReader config, LOG.debug("Using internal security mechanisms."); final AuthenticationManager authManager = buildAuthenticationManager(http); http.authorizeHttpRequests( - (auth) -> auth.requestMatchers("/rest/users/impersonate").hasRole(SecurityConstants.ROLE_ADMIN) + (auth) -> auth.requestMatchers("/rest/users/impersonate").hasAuthority(SecurityConstants.ROLE_ADMIN) .anyRequest().permitAll()) .cors((auth) -> auth.configurationSource(corsConfigurationSource(config))) .csrf(AbstractHttpConfigurer::disable) diff --git a/src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java b/src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java index e8a51bcd..80723dc4 100644 --- a/src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java +++ b/src/main/java/cz/cvut/kbss/study/security/CustomSwitchUserFilter.java @@ -6,6 +6,9 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; +/** + * Extends default user switching logic by preventing switching to an admin account. + */ public class CustomSwitchUserFilter extends SwitchUserFilter { @Override diff --git a/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java b/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java index 48fd9bf6..b5af68bf 100644 --- a/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java +++ b/src/main/java/cz/cvut/kbss/study/service/security/SecurityUtils.java @@ -3,6 +3,7 @@ import cz.cvut.kbss.study.exception.NotFoundException; import cz.cvut.kbss.study.model.PatientRecord; import cz.cvut.kbss.study.model.User; +import cz.cvut.kbss.study.model.Vocabulary; import cz.cvut.kbss.study.persistence.dao.PatientRecordDao; import cz.cvut.kbss.study.persistence.dao.UserDao; import cz.cvut.kbss.study.security.model.Role; @@ -16,6 +17,7 @@ import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.web.server.authentication.SwitchUserWebFilter; import org.springframework.stereotype.Service; import java.util.List; @@ -70,9 +72,13 @@ public User getCurrentUser() { if (principal instanceof Jwt) { return resolveAccountFromOAuthPrincipal((Jwt) principal); } else { - assert principal instanceof UserDetails; final String username = context.getAuthentication().getName(); - return userDao.findByUsername(username); + final User user = userDao.findByUsername(username); + if (context.getAuthentication().getAuthorities().stream().anyMatch(a -> a.getAuthority().equals( + SwitchUserWebFilter.ROLE_PREVIOUS_ADMINISTRATOR))) { + user.addType(Vocabulary.s_c_impersonator); + } + return user; } } diff --git a/src/main/resources/model.ttl b/src/main/resources/model.ttl index e290cd26..39067f2b 100644 --- a/src/main/resources/model.ttl +++ b/src/main/resources/model.ttl @@ -159,5 +159,9 @@ rm:patient-record rdf:type owl:Class ; rm:user rdf:type owl:Class ; rdfs:label "User"@en . +### http://onto.fel.cvut.cz/ontologies/record-manager/impersonator +rm:impersonator rdf:type owl:Class ; + rdfs:label "Impersonator"@en . + ### Generated by the OWL API (version 4.2.8.20170104-2310) https://github.com/owlcs/owlapi diff --git a/src/test/java/cz/cvut/kbss/study/service/security/SecurityUtilsTest.java b/src/test/java/cz/cvut/kbss/study/service/security/SecurityUtilsTest.java index 4cdcdd11..9a3894f4 100644 --- a/src/test/java/cz/cvut/kbss/study/service/security/SecurityUtilsTest.java +++ b/src/test/java/cz/cvut/kbss/study/service/security/SecurityUtilsTest.java @@ -9,6 +9,7 @@ import cz.cvut.kbss.study.persistence.dao.PatientRecordDao; import cz.cvut.kbss.study.persistence.dao.UserDao; import cz.cvut.kbss.study.security.SecurityConstants; +import cz.cvut.kbss.study.security.model.UserDetails; import cz.cvut.kbss.study.service.ConfigReader; import cz.cvut.kbss.study.util.ConfigParam; import cz.cvut.kbss.study.util.IdentificationUtils; @@ -19,15 +20,18 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.web.authentication.switchuser.SwitchUserFilter; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.List; +import java.util.Set; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; @@ -183,4 +187,15 @@ void getCurrentUserEnhancesRetrievedUserWithTypesCorrespondingToRolesSpecifiedIn final User result = sut.getCurrentUser(); assertThat(result.getTypes(), hasItem(Vocabulary.s_c_administrator)); } + + @Test + void getCurrentUserEnhancesRetrievedUserWithImpersonatorTypeWhenItHasSwitchAuthorityRole() { + final UserDetails userDetails = + new UserDetails(user, Set.of(new SimpleGrantedAuthority(SwitchUserFilter.ROLE_PREVIOUS_ADMINISTRATOR))); + SecurityUtils.setCurrentUser(userDetails); + when(userDao.findByUsername(user.getUsername())).thenReturn(user); + final User result = sut.getCurrentUser(); + assertEquals(user, result); + assertThat(result.getTypes(), hasItem(Vocabulary.s_c_impersonator)); + } } From b1695f2ec010b7e390b82547002ea31a7c61a106 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka <martin.ledvinka@fel.cvut.cz> Date: Thu, 23 Nov 2023 10:16:34 +0100 Subject: [PATCH 3/7] [Fix] Fix docker compose configuration issues after renaming the app repo and backend service. --- ...config-record-manager.ttl => config-record-manager-app.ttl} | 0 docker-compose.yml | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename db-server/repo-config/{config-record-manager.ttl => config-record-manager-app.ttl} (100%) diff --git a/db-server/repo-config/config-record-manager.ttl b/db-server/repo-config/config-record-manager-app.ttl similarity index 100% rename from db-server/repo-config/config-record-manager.ttl rename to db-server/repo-config/config-record-manager-app.ttl diff --git a/docker-compose.yml b/docker-compose.yml index c7b655ee..b09bd5c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,7 @@ services: FORMGENREPOSITORYURL: "http://db-server:7200/repositories/record-manager-formgen" FORMGENSERVICEURL: "http://s-pipes-engine:8080/s-pipes/service?_pId=clone&sgovRepositoryUrl=https%3A%2F%2Fgraphdb.onto.fel.cvut.cz%2Frepositories%2Fkodi-slovnik-gov-cz" SECURITY_PROVIDER: "oidc" + SERVER_SERVLET_CONTEXTPATH: "/record-manager-server" SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI: "http://localhost:8088/realms/record-manager" SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWKSETURI: "http://auth-server:8080/realms/record-manager/protocol/openid-connect/certs" @@ -79,7 +80,7 @@ services: DB_PASSWORD: keycloak DB_SCHEMA: "public" DB_SERVER_URL: "http://db-server:7200" - DB_SERVER_REPOSITORY_ID: "record-manager" + DB_SERVER_REPOSITORY_ID: "record-manager-app" REPOSITORY_LANGUAGE: "en" VOCABULARY_USER_TYPE: "http://onto.fel.cvut.cz/ontologies/record-manager/user" VOCABULARY_USER_FIRST_NAME: "http://xmlns.com/foaf/0.1/firstName" From e370a5e264df53d87ff36697669982e4802118bd Mon Sep 17 00:00:00 2001 From: Martin Ledvinka <martin.ledvinka@fel.cvut.cz> Date: Thu, 23 Nov 2023 13:07:58 +0100 Subject: [PATCH 4/7] [kbss-cvut/23ava-distribution#18] Add getByUsername endpoint back to OIDC-based users REST controller. It is needed for impersonate functionality, as it needs to open the user detail. --- .../cz/cvut/kbss/study/rest/OidcUserController.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/cz/cvut/kbss/study/rest/OidcUserController.java b/src/main/java/cz/cvut/kbss/study/rest/OidcUserController.java index c55d3f21..0c01c193 100644 --- a/src/main/java/cz/cvut/kbss/study/rest/OidcUserController.java +++ b/src/main/java/cz/cvut/kbss/study/rest/OidcUserController.java @@ -1,5 +1,6 @@ package cz.cvut.kbss.study.rest; +import cz.cvut.kbss.study.exception.NotFoundException; import cz.cvut.kbss.study.model.Institution; import cz.cvut.kbss.study.model.User; import cz.cvut.kbss.study.security.SecurityConstants; @@ -9,6 +10,7 @@ import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -40,6 +42,17 @@ public User getCurrent() { return userService.getCurrentUser(); } + @PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or #username == authentication.name or " + + "hasRole('" + SecurityConstants.ROLE_USER + "') and @securityUtils.areFromSameInstitution(#username)") + @GetMapping(value = "/{username}", produces = MediaType.APPLICATION_JSON_VALUE) + public User getByUsername(@PathVariable("username") String username) { + final User user = userService.findByUsername(username); + if (user == null) { + throw NotFoundException.create("User", username); + } + return user; + } + @PreAuthorize( "hasRole('" + SecurityConstants.ROLE_ADMIN + "') " + "or hasRole('" + SecurityConstants.ROLE_USER + "') and @securityUtils.isMemberOfInstitution(#institutionKey)") From 8ad489d81af4912ddb28d3dc25f97f936a6df6d6 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka <martin.ledvinka@fel.cvut.cz> Date: Thu, 23 Nov 2023 13:08:15 +0100 Subject: [PATCH 5/7] [kbss-cvut/23ava-distribution#18] Enable features required for impersonation in Keycloak. --- docker-compose.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b09bd5c9..a94f6d7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,9 @@ version: '3.9' services: record-manager: - image: 'ghcr.io/kbss-cvut/kbss-cvut/record-manager-ui:latest' + build: /home/kidney/Development/Javascript/Projects/AKAENE/record-manager-ui + image: record-manager-ui + container_name: record-manager ports: - "127.0.0.1:3000:80" depends_on: @@ -64,7 +66,7 @@ services: auth-server: image: "ghcr.io/kbss-cvut/keycloak-graphdb-user-replicator/keycloak-graphdb:latest" command: - - start --import-realm + - start --import-realm --features="token-exchange,admin-fine-grained-authz" environment: KC_IMPORT: realm-export.json KC_HOSTNAME_URL: "http://localhost:8088" From eb1959438c0fa838ea82c4a6e183f918aa57b237 Mon Sep 17 00:00:00 2001 From: Martin Ledvinka <martin.ledvinka@fel.cvut.cz> Date: Wed, 29 Nov 2023 15:46:08 +0100 Subject: [PATCH 6/7] [Fix] Fix accidental change of frontend image in docker-compose.yml. --- docker-compose.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index a94f6d7d..7f4a5aef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,7 @@ version: '3.9' services: record-manager: - build: /home/kidney/Development/Javascript/Projects/AKAENE/record-manager-ui - image: record-manager-ui - container_name: record-manager + image: 'ghcr.io/kbss-cvut/kbss-cvut/record-manager-ui:latest' ports: - "127.0.0.1:3000:80" depends_on: From 48b8ddeda7105f288238e1b3af39798ea848c90e Mon Sep 17 00:00:00 2001 From: Martin Ledvinka <martin.ledvinka@fel.cvut.cz> Date: Wed, 29 Nov 2023 15:57:41 +0100 Subject: [PATCH 7/7] [Fix] Fix upstream merge issues. --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7db84b36..7f4a5aef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,6 @@ services: SERVER_SERVLET_CONTEXTPATH: "/record-manager-server" SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI: "http://localhost:8088/realms/record-manager" SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWKSETURI: "http://auth-server:8080/realms/record-manager/protocol/openid-connect/certs" - SERVER_SERVLET_CONTEXTPATH: "/record-manager-server" s-pipes-engine: image: "ghcr.io/kbss-cvut/s-pipes/s-pipes-engine:latest"