Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
blcham committed Dec 1, 2023
2 parents 001a075 + aa48e0b commit f141fbd
Show file tree
Hide file tree
Showing 15 changed files with 213 additions and 76 deletions.
33 changes: 33 additions & 0 deletions db-server/repo-config/config-record-manager-app.ttl
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rep: <http://www.openrdf.org/config/repository#> .
@prefix sail: <http://www.openrdf.org/config/sail#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix graphdb: <http://www.ontotext.com/config/graphdb#>.

<#record-manager-app> a rep:Repository;
rep:repositoryID "record-manager-app";
rep:repositoryImpl [
rep:repositoryType "graphdb:SailRepository";
<http://www.openrdf.org/config/repository/sail#sailImpl> [
graphdb:base-URL "http://example.org/owlim#";
graphdb:check-for-inconsistencies "false";
graphdb:defaultNS "";
graphdb:disable-sameAs "true";
graphdb:enable-context-index "true";
graphdb:enable-literal-index "true";
graphdb:enablePredicateList "true";
graphdb:entity-id-size "32";
graphdb:entity-index-size "10000000";
graphdb:imports "";
graphdb:in-memory-literal-properties "true";
graphdb:owlim-license "";
graphdb:query-limit-results "0";
graphdb:query-timeout "0";
graphdb:read-only "false";
graphdb:repository-type "file-repository";
graphdb:storage-folder "storage";
graphdb:throw-QueryEvaluationException-on-timeout "false";
sail:sailType "graphdb:Sail"
]
];
rdfs:label "Record Manager Repository" .
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ 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"
SERVER_SERVLET_CONTEXTPATH: "/record-manager-server"

s-pipes-engine:
image: "ghcr.io/kbss-cvut/s-pipes/s-pipes-engine:latest"
Expand Down Expand Up @@ -64,7 +64,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"
Expand Down
30 changes: 27 additions & 3 deletions src/main/java/cz/cvut/kbss/study/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,18 +22,24 @@
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;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

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
Expand Down Expand Up @@ -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").hasAuthority(SecurityConstants.ROLE_ADMIN)
.anyRequest().permitAll())
.cors((auth) -> auth.configurationSource(corsConfigurationSource(config)))
.csrf(AbstractHttpConfigurer::disable)
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
Expand All @@ -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();
}
Expand Down Expand Up @@ -139,4 +152,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;
}
}
13 changes: 13 additions & 0 deletions src/main/java/cz/cvut/kbss/study/rest/OidcUserController.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)")
Expand Down
18 changes: 0 additions & 18 deletions src/main/java/cz/cvut/kbss/study/rest/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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;

/**
* Extends default user switching logic by preventing switching to an admin account.
*/
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public String getUsername() {

@Override
public boolean isAccountNonExpired() {
return false;
return true;
}

@Override
Expand All @@ -86,4 +86,12 @@ public boolean isEnabled() {
public User getUser() {
return user;
}

@Override
public String toString() {
return "UserDetails{" +
"user=" + user +
", authorities=" + authorities +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -40,15 +42,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

Expand All @@ -70,9 +72,13 @@ public User getCurrentUser() {
if (principal instanceof Jwt) {
return resolveAccountFromOAuthPrincipal((Jwt) principal);
} else {
assert principal instanceof String;
final String username = context.getAuthentication().getPrincipal().toString();
return userDao.findByUsername(username);
final String username = context.getAuthentication().getName();
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;
}
}

Expand All @@ -81,7 +87,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;
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/model.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading

0 comments on commit f141fbd

Please sign in to comment.