From c61280716810286ac2f65307649fd5a01c9b1c75 Mon Sep 17 00:00:00 2001 From: Francois Prunayre Date: Mon, 19 Aug 2024 12:35:40 +0200 Subject: [PATCH] OAuth / Create user if it does not exist. --- .../config/WebSecurityConfiguration.java | 108 +++++++++++++++++- src/main/java/org/geonetwork/domain/User.java | 5 + .../geonetwork/repository/UserRepository.java | 2 + .../security/DatabaseUserDetailsService.java | 2 + .../java/org/geonetwork/security/MeApi.java | 15 ++- 5 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/geonetwork/config/WebSecurityConfiguration.java b/src/main/java/org/geonetwork/config/WebSecurityConfiguration.java index 30f16ec6..2b1993f9 100644 --- a/src/main/java/org/geonetwork/config/WebSecurityConfiguration.java +++ b/src/main/java/org/geonetwork/config/WebSecurityConfiguration.java @@ -3,8 +3,25 @@ * This code is licensed under the GPL 2.0 license, * available at the root application directory. */ + package org.geonetwork.config; +import static org.geonetwork.security.DatabaseUserDetailsService.GN_AUTHORITY; +import static org.geonetwork.security.DatabaseUserDetailsService.HIGHEST_PROFILE; +import static org.geonetwork.security.DatabaseUserDetailsService.USER_ID; +import static org.geonetwork.security.DatabaseUserDetailsService.USER_NAME; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.geonetwork.domain.Profile; +import org.geonetwork.domain.User; +import org.geonetwork.domain.Usergroup; import org.geonetwork.proxy.HttpProxyPolicyAgentAuthorizationManager; import org.geonetwork.repository.UserRepository; import org.geonetwork.repository.UsergroupRepository; @@ -17,9 +34,13 @@ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.StandardPasswordEncoder; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -31,7 +52,9 @@ public class WebSecurityConfiguration { @Bean public SecurityFilterChain securityFilterChain( HttpSecurity http, - HttpProxyPolicyAgentAuthorizationManager proxyPolicyAgentAuthorizationManager) + HttpProxyPolicyAgentAuthorizationManager proxyPolicyAgentAuthorizationManager, + UserRepository userRepository, + UsergroupRepository userGroupRepository) throws Exception { http.authorizeHttpRequests( requests -> @@ -44,6 +67,15 @@ public SecurityFilterChain securityFilterChain( .access(proxyPolicyAgentAuthorizationManager) .anyRequest() .permitAll()) + .oauth2Login( + oauth -> + oauth + .permitAll() + .userInfoEndpoint( + userInfo -> + userInfo.userAuthoritiesMapper( + this.userOauthAuthoritiesMapper( + userRepository, userGroupRepository)))) .formLogin( form -> form.loginPage("/signin") @@ -84,4 +116,78 @@ public UserDetailsService userDetailsService( return new DatabaseUserDetailsService( checkUsernameOrEmail, passwordEncoder, userRepository, userGroupRepository); } + + private GrantedAuthoritiesMapper userOauthAuthoritiesMapper( + UserRepository userRepository, UsergroupRepository userGroupRepository) { + return (authorities) -> { + Set mappedAuthorities = new HashSet<>(); + + authorities.forEach( + authority -> { + if (authority instanceof OidcUserAuthority) { + // Handle OIDC user authority + } else if (authority instanceof OAuth2UserAuthority oauth2UserAuthority) { + Map userAttributes = oauth2UserAuthority.getAttributes(); + String email = userAttributes.get("email").toString(); + Optional oauthUser = userRepository.findOptionalByEmail(email); + + User user = + oauthUser.orElseGet( + () -> { + String username = + Optional.ofNullable(userAttributes.get("login")) + .orElse(email) + .toString(); + User newUser = + User.builder() + .isenabled("y") + .password("") + .username(username) + .name( + Optional.ofNullable(userAttributes.get("name")) + .orElse("") + .toString()) + .surname("") + .authtype(authority.getAuthority()) + .email(Set.of(email)) + .organisation( + Optional.ofNullable(userAttributes.get("company")) + .orElse("") + .toString()) + .build(); + userRepository.save(newUser); + return newUser; + }); + + user.setUsername( + Optional.ofNullable(userAttributes.get("name")).orElse(email).toString()); + userRepository.save(user); + + String mainUserProfile = user.getProfile().name(); + List userGroups = userGroupRepository.findAllByUserid_Id(user.getId()); + + Map> attributesToCast = + userGroups.stream() + .collect( + Collectors.groupingBy( + ug -> Profile.values()[ug.getId().getProfile()].name(), + Collectors.mapping( + ug -> ug.getGroupid().getId(), Collectors.toList()))); + + Map attributes = new HashMap<>(attributesToCast); + attributes.put(USER_ID, user.getId()); + attributes.put(USER_NAME, user.getUsername()); + attributes.put(HIGHEST_PROFILE, mainUserProfile); + attributes.putIfAbsent(Profile.UserAdmin.name(), Collections.emptyList()); + attributes.putIfAbsent(Profile.Reviewer.name(), Collections.emptyList()); + attributes.putIfAbsent(Profile.RegisteredUser.name(), Collections.emptyList()); + attributes.putIfAbsent(Profile.Editor.name(), Collections.emptyList()); + + mappedAuthorities.add(new OAuth2UserAuthority(GN_AUTHORITY, attributes)); + } + }); + + return mappedAuthorities; + }; + } } diff --git a/src/main/java/org/geonetwork/domain/User.java b/src/main/java/org/geonetwork/domain/User.java index 7b00e0fc..bd41b00f 100644 --- a/src/main/java/org/geonetwork/domain/User.java +++ b/src/main/java/org/geonetwork/domain/User.java @@ -13,10 +13,13 @@ import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinTable; import jakarta.persistence.OneToMany; +import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -39,6 +42,8 @@ @Table(name = "users") public class User { @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_id_gen") + @SequenceGenerator(name = "user_id_gen", sequenceName = "user_id_seq", allocationSize = 1) @Column(name = "id", nullable = false) private Integer id; diff --git a/src/main/java/org/geonetwork/repository/UserRepository.java b/src/main/java/org/geonetwork/repository/UserRepository.java index b10c9e5c..9852d088 100644 --- a/src/main/java/org/geonetwork/repository/UserRepository.java +++ b/src/main/java/org/geonetwork/repository/UserRepository.java @@ -16,5 +16,7 @@ public interface UserRepository extends JpaRepository { Optional findOptionalByEmailAndAuthtypeIsNull(String email); + Optional findOptionalByEmail(String email); + Optional findOptionalByUsernameOrEmailAndAuthtypeIsNull(String username, String email); } diff --git a/src/main/java/org/geonetwork/security/DatabaseUserDetailsService.java b/src/main/java/org/geonetwork/security/DatabaseUserDetailsService.java index fc5cc2ec..1613900d 100644 --- a/src/main/java/org/geonetwork/security/DatabaseUserDetailsService.java +++ b/src/main/java/org/geonetwork/security/DatabaseUserDetailsService.java @@ -38,6 +38,7 @@ public class DatabaseUserDetailsService extends AbstractUserDetailsAuthenticatio public static final String HIGHEST_PROFILE = "highest_profile"; public static final String USER_ID = "user_id"; + public static final String USER_NAME = "username"; public static final String GN_AUTHORITY = "gn"; @Setter @Getter private DatabaseUserAuthProperties checkUsernameOrEmail; @@ -110,6 +111,7 @@ protected UserDetails retrieveUser( String mainUserProfile = user.get().getProfile().name(); Map attributes = new HashMap<>(attributesToCast); attributes.put(USER_ID, user.get().getId()); + attributes.put(USER_NAME, user.get().getUsername()); attributes.put(HIGHEST_PROFILE, mainUserProfile); attributes.putIfAbsent(Profile.UserAdmin.name(), Collections.emptyList()); attributes.putIfAbsent(Profile.Reviewer.name(), Collections.emptyList()); diff --git a/src/main/java/org/geonetwork/security/MeApi.java b/src/main/java/org/geonetwork/security/MeApi.java index b04d59c6..454765ca 100644 --- a/src/main/java/org/geonetwork/security/MeApi.java +++ b/src/main/java/org/geonetwork/security/MeApi.java @@ -5,14 +5,18 @@ */ package org.geonetwork.security; +import static org.geonetwork.security.DatabaseUserDetailsService.USER_NAME; + import java.util.Collections; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -27,10 +31,15 @@ public class MeApi { @PreAuthorize("permitAll") public ResponseEntity> user( @AuthenticationPrincipal AuthenticationPrincipal principal, + Authentication authentication, @AuthenticationPrincipal UserDetails userDetails) { String userName = ""; - if (principal instanceof OAuth2User) { - userName = ((OAuth2User) principal).getAttribute("name"); + if (authentication instanceof OAuth2AuthenticationToken) { + userName = + ((OAuth2UserAuthority) authentication.getAuthorities().toArray()[0]) + .getAttributes() + .get(USER_NAME) + .toString(); } else if (userDetails != null) { userName = userDetails.getUsername(); }