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"