Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/frontend/node_modules/
/.stored_data/
/.env
/.playwright-cli/
8 changes: 0 additions & 8 deletions .idea/.gitignore

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/datacatalog.iml

This file was deleted.

18 changes: 0 additions & 18 deletions .idea/inspectionProfiles/Project_Default.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/inspectionProfiles/profiles_settings.xml

This file was deleted.

7 changes: 0 additions & 7 deletions .idea/misc.xml

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package ebrainsv2.mip.datacatalog.configurations;

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
Expand All @@ -9,21 +8,36 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.stereotype.Component;

import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

Expand All @@ -38,6 +52,9 @@ public class SecurityConfiguration {
@Value("${app.frontend.base-url}")
private String frontendBaseUrl;

@Value("${spring.security.oauth2.client.registration.keycloak.client-id:datacatalog}")
private String oidcClientId;

@Value("${authentication.enabled}")
private boolean authenticationEnabled;

Expand All @@ -63,11 +80,18 @@ public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRe
OAuth2AuthorizedClientService authorizedClientService) throws Exception {

if (authenticationEnabled) {
CookieCsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
csrfTokenRepository.setCookiePath("/");
csrfTokenRepository.setCookieName("MIP-XSRF-TOKEN");
csrfTokenRepository.setHeaderName("X-MIP-XSRF-TOKEN");

http
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // Allow access to any endpoint unless restricted by @PreAuthorize
)
.oauth2Login(oauth -> oauth
.userInfoEndpoint(userInfo -> userInfo.oidcUserService(oidcUserService()))
.defaultSuccessUrl(this.authCallbackUrl, true)
.successHandler((request, response, authentication) -> {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
Expand All @@ -88,7 +112,11 @@ public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRe
successHandler.setPostLogoutRedirectUri(this.frontendBaseUrl);
logout.logoutSuccessHandler(successHandler);
})
.csrf(AbstractHttpConfigurer::disable); // Ensure CSRF protection as per requirements
.csrf(csrf -> csrf
.csrfTokenRepository(csrfTokenRepository)
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class);
} else {
http
.authorizeHttpRequests(auth -> auth
Expand All @@ -99,26 +127,106 @@ public SecurityFilterChain clientSecurityFilterChain(HttpSecurity http, ClientRe
return http.build();
}

@Component
@RequiredArgsConstructor
static class GrantedAuthoritiesMapperImpl implements GrantedAuthoritiesMapper {
private static Collection<GrantedAuthority> extractAuthorities(Map<String, Object> claims) {
return ((Collection<String>) claims.get("authorities")).stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

@Override
public Collection<? extends GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) {
@Bean
public GrantedAuthoritiesMapper grantedAuthoritiesMapper() {
return authorities -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

authorities.forEach(authority -> {
mappedAuthorities.add(authority);

if (authority instanceof OidcUserAuthority oidcUserAuthority) {
mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getIdToken().getClaims()));
mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getIdToken().getClaims(), oidcClientId));
if (oidcUserAuthority.getUserInfo() != null) {
mappedAuthorities.addAll(extractAuthorities(oidcUserAuthority.getUserInfo().getClaims(), oidcClientId));
}
}
});

return mappedAuthorities;
};
}

@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
OidcUserService delegate = new OidcUserService();
return (OidcUserRequest userRequest) -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
Set<GrantedAuthority> mappedAuthorities = new LinkedHashSet<>();
mappedAuthorities.addAll(grantedAuthoritiesMapper().mapAuthorities(oidcUser.getAuthorities()));

String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();

if (userNameAttributeName == null || userNameAttributeName.isBlank()) {
return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());
}

return new DefaultOidcUser(
mappedAuthorities,
oidcUser.getIdToken(),
oidcUser.getUserInfo(),
userNameAttributeName
);
};
}

private static Collection<GrantedAuthority> extractAuthorities(Map<String, Object> claims, String clientId) {
Set<String> authorities = new LinkedHashSet<>();

authorities.addAll(asStringCollection(claims.get("authorities")));
authorities.addAll(asStringCollection(claims.get("roles")));

Object realmAccess = claims.get("realm_access");
if (realmAccess instanceof Map<?, ?> realmAccessMap) {
authorities.addAll(asStringCollection(realmAccessMap.get("roles")));
}

Object resourceAccess = claims.get("resource_access");
if (resourceAccess instanceof Map<?, ?> resourceAccessMap) {
Object clientAccess = resourceAccessMap.get(clientId);
if (clientAccess instanceof Map<?, ?> clientAccessMap) {
authorities.addAll(asStringCollection(clientAccessMap.get("roles")));
}
}

return authorities.stream()
.filter(Objects::nonNull)
.filter(role -> !role.isBlank())
.flatMap(role -> normalizeAuthorities(role).stream())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toCollection(LinkedHashSet::new));
}

private static Collection<String> normalizeAuthorities(String role) {
String normalizedRole = role.trim();
if (normalizedRole.startsWith("ROLE_")) {
return List.of(normalizedRole, normalizedRole.substring("ROLE_".length()));
}
return List.of(normalizedRole, "ROLE_" + normalizedRole);
}

private static Collection<String> asStringCollection(Object value) {
if (value instanceof Collection<?> collection) {
return collection.stream()
.filter(Objects::nonNull)
.map(String::valueOf)
.toList();
}
return List.of();
}

static class CsrfCookieFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrfToken != null) {
csrfToken.getToken();
}
filterChain.doFilter(request, response);
}
}
}
4 changes: 2 additions & 2 deletions frontend/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
"maximumWarning": "4kB",
"maximumError": "6kB"
}
],
"outputHashing": "all",
Expand Down
Loading
Loading