From 10687ffe5df00d2845ddd82c08ff892dd8a3442b Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 2 Oct 2025 21:46:38 +0200 Subject: [PATCH] - Integrated entra id into "prod"-profile --- pom.xml | 4 + .../java/de/avatic/lcc/config/CorsConfig.java | 57 ++++++++++++++ .../de/avatic/lcc/config/FrontendConfig.java | 2 + .../de/avatic/lcc/config/LccOidcUser.java | 27 +++++++ .../de/avatic/lcc/config/SecurityConfig.java | 77 +++++++++++++++++-- .../repositories/users/UserRepository.java | 48 +++++++----- .../lcc/service/access/PremisesService.java | 12 +++ src/main/resources/application.properties | 8 +- 8 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 src/main/java/de/avatic/lcc/config/CorsConfig.java create mode 100644 src/main/java/de/avatic/lcc/config/LccOidcUser.java diff --git a/pom.xml b/pom.xml index 4bc888a..2cdaeab 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,10 @@ com.azure.spring spring-cloud-azure-starter-jdbc-mysql + + org.springframework.boot + spring-boot-starter-oauth2-client + org.springframework.boot spring-boot-starter-validation diff --git a/src/main/java/de/avatic/lcc/config/CorsConfig.java b/src/main/java/de/avatic/lcc/config/CorsConfig.java new file mode 100644 index 0000000..cd65c2e --- /dev/null +++ b/src/main/java/de/avatic/lcc/config/CorsConfig.java @@ -0,0 +1,57 @@ +package de.avatic.lcc.config; + +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; + +@Configuration +@EnableWebMvc +@Order(Ordered.HIGHEST_PRECEDENCE) +@Profile("dev | test") +public class CorsConfig implements WebMvcConfigurer { + + @Autowired + private Environment environment; + + @Value("${lcc.allowed_cors}") + private String allowedCors; + + @Override + public void addCorsMappings(@NotNull CorsRegistry registry) { + String[] activeProfiles = environment.getActiveProfiles(); + + System.out.println("Active profiles: " + Arrays.toString(activeProfiles)); + System.out.println("Allowed CORS: " + allowedCors); + + if (Arrays.asList(activeProfiles).contains("dev")) { + + System.out.println("Applying DEV CORS configuration"); + + // Development CORS configuration + registry.addMapping("/api/**") + .allowedOriginPatterns("http://localhost:*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") + .allowCredentials(false); + } else { + // Production CORS configuration + registry.addMapping("/api/**") + .allowedOrigins(allowedCors) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .exposedHeaders("X-Total-Count", "X-Page-Count", "X-Current-Page") + .allowCredentials(true); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/config/FrontendConfig.java b/src/main/java/de/avatic/lcc/config/FrontendConfig.java index 1ddf78c..5d7dce3 100644 --- a/src/main/java/de/avatic/lcc/config/FrontendConfig.java +++ b/src/main/java/de/avatic/lcc/config/FrontendConfig.java @@ -2,6 +2,7 @@ package de.avatic.lcc.config; import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; @@ -10,6 +11,7 @@ import org.springframework.web.servlet.resource.PathResourceResolver; import java.io.IOException; @Configuration +@Profile("!dev & !test") public class FrontendConfig implements WebMvcConfigurer { @Override diff --git a/src/main/java/de/avatic/lcc/config/LccOidcUser.java b/src/main/java/de/avatic/lcc/config/LccOidcUser.java new file mode 100644 index 0000000..b21403d --- /dev/null +++ b/src/main/java/de/avatic/lcc/config/LccOidcUser.java @@ -0,0 +1,27 @@ +package de.avatic.lcc.config; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; + +import java.util.Collection; + +public class LccOidcUser extends DefaultOidcUser { + + private final Integer userId; + + public LccOidcUser(Collection authorities, + OidcIdToken idToken, + OidcUserInfo userInfo, + String nameAttributeKey, + Integer userId) { + super(authorities, idToken, userInfo, nameAttributeKey); + this.userId = userId; + } + + public Integer getSqlUserId() { + return userId; + } + +} diff --git a/src/main/java/de/avatic/lcc/config/SecurityConfig.java b/src/main/java/de/avatic/lcc/config/SecurityConfig.java index a3b091d..d24de23 100644 --- a/src/main/java/de/avatic/lcc/config/SecurityConfig.java +++ b/src/main/java/de/avatic/lcc/config/SecurityConfig.java @@ -1,26 +1,39 @@ package de.avatic.lcc.config; +import de.avatic.lcc.model.users.User; +import de.avatic.lcc.repositories.users.GroupRepository; +import de.avatic.lcc.repositories.users.UserRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; -import java.util.List; +import java.util.HashSet; +import java.util.Set; @Configuration public class SecurityConfig { - @Bean @Profile("!dev & !test") // Only active when NOT in dev profile public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - // Your production security configuration + http + .authorizeHttpRequests(auth -> auth + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .defaultSuccessUrl("/", true) + ); + return http.build(); } @@ -32,4 +45,54 @@ public class SecurityConfig { .csrf(AbstractHttpConfigurer::disable) .build(); } + + @Bean + @Profile("!dev & !test") + public OAuth2UserService oidcUserService(UserRepository userRepository, GroupRepository groupRepository) { + final OidcUserService delegate = new OidcUserService(); + + return (userRequest) -> { + OidcUser oidcUser = delegate.loadUser(userRequest); + Integer userId = null; + +// // Debug: Print all claims +// System.out.println("=== ID Token Claims ==="); +// oidcUser.getIdToken().getClaims().forEach((key, value) -> +// System.out.println(key + ": " + value) +// ); +// System.out.println("======================"); + + Set mappedAuthorities = new HashSet<>(oidcUser.getAuthorities()); + + // Try different ways to get email + String email = oidcUser.getEmail(); + if (email == null) { + email = oidcUser.getAttribute("email"); + } + if (email == null) { + email = oidcUser.getAttribute("preferred_username"); + } + if (email == null) { + email = oidcUser.getAttribute("upn"); + } + + if (email != null) { + User user = userRepository.getByEmail(email); + if (user != null) { + user.getGroups().forEach(group -> mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + group.getName()))); + userId = user.getId(); + } else { + mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_default")); + } + } + + return new LccOidcUser( + mappedAuthorities, + oidcUser.getIdToken(), + oidcUser.getUserInfo(), + "preferred_username", + userId + ); + }; + } } diff --git a/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java b/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java index 344cfb5..c40434f 100644 --- a/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java +++ b/src/main/java/de/avatic/lcc/repositories/users/UserRepository.java @@ -5,7 +5,6 @@ import de.avatic.lcc.model.users.User; import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryResult; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; @@ -19,7 +18,6 @@ import java.sql.Statement; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; @Repository public class UserRepository { @@ -69,7 +67,7 @@ public class UserRepository { List groupIds = findGroupIds(user.getGroups().stream().map(Group::getName).toList()); - if(userId == null) { + if (userId == null) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement( @@ -86,15 +84,14 @@ public class UserRepository { }, keyHolder); userId = Objects.requireNonNull(keyHolder.getKey()).intValue(); - } - else { + } else { String query = """ - UPDATE sys_user SET email = ?, firstname = ?, lastname = ?, workday_id = ?, is_active = ? WHERE id = ?"""; + UPDATE sys_user SET email = ?, firstname = ?, lastname = ?, workday_id = ?, is_active = ? WHERE id = ?"""; jdbcTemplate.update(query, user.getEmail(), user.getFirstName(), user.getLastName(), user.getWorkdayId(), user.getActive(), userId); } - updateUserGroupMappings(userId, groupIds); + updateUserGroupMappings(userId, groupIds); } @@ -159,27 +156,42 @@ public class UserRepository { FROM sys_user WHERE id = ?"""; - return jdbcTemplate.queryForObject(query, new UserMapper(), id); + + var user = jdbcTemplate.query(query, new UserMapper(), id); + + return user.isEmpty() ? null : user.getFirst(); + } + + @Transactional + public User getByEmail(String email) { + String query = """ + SELECT * + FROM sys_user + WHERE email = ?"""; + + var user = jdbcTemplate.query(query, new UserMapper(), email); + + return user.isEmpty() ? null : user.getFirst(); } private class UserMapper implements RowMapper { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { - var user = new User(); + var user = new User(); - int id = rs.getInt("id"); + int id = rs.getInt("id"); - user.setId(id); - user.setActive(rs.getBoolean("is_active")); - user.setEmail(rs.getString("email")); - user.setFirstName(rs.getString("firstname")); - user.setLastName(rs.getString("lastname")); - user.setWorkdayId(rs.getString("workday_id")); + user.setId(id); + user.setActive(rs.getBoolean("is_active")); + user.setEmail(rs.getString("email")); + user.setFirstName(rs.getString("firstname")); + user.setLastName(rs.getString("lastname")); + user.setWorkdayId(rs.getString("workday_id")); - user.setGroups(getGroupMemberships(id)); + user.setGroups(getGroupMemberships(id)); - return user; + return user; } } diff --git a/src/main/java/de/avatic/lcc/service/access/PremisesService.java b/src/main/java/de/avatic/lcc/service/access/PremisesService.java index d1d6139..24fc0eb 100644 --- a/src/main/java/de/avatic/lcc/service/access/PremisesService.java +++ b/src/main/java/de/avatic/lcc/service/access/PremisesService.java @@ -1,5 +1,6 @@ package de.avatic.lcc.service.access; +import de.avatic.lcc.config.LccOidcUser; import de.avatic.lcc.dto.calculation.CalculationStatus; import de.avatic.lcc.dto.calculation.PremiseDTO; import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; @@ -27,6 +28,9 @@ import de.avatic.lcc.service.transformer.premise.PremiseTransformer; import de.avatic.lcc.util.exception.base.InternalErrorException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -84,6 +88,14 @@ public class PremisesService { //TODO use actual user. userId = 1; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + //todo make a service. and simulate user rights in dev profile. + if (authentication != null && authentication.getPrincipal() instanceof LccOidcUser) { + LccOidcUser oidcUser = (LccOidcUser) authentication.getPrincipal(); + } + + return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, deleted, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6fc17b3..8c81fe0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,7 +5,6 @@ spring.datasource.username=${DB_USER} spring.datasource.password=${DB_PASSWORD} spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.sql.init.mode=never -#spring.profiles.active=setup lcc.bulk.sheet_password=secretSheet?! lcc.allowed_cors=${ALLOWED_CORS_DOMAIN} azure.maps.subscription.key=${AZURE_MAPS_SUBSCRIPTION_KEY} @@ -13,5 +12,8 @@ azure.maps.client.id=your-app-registration-client-id azure.maps.resource.id=/subscriptions/sub-id/resourceGroups/rg-name/providers/Microsoft.Maps/accounts/account-name spring.servlet.multipart.max-file-size=30MB spring.servlet.multipart.max-request-size=50MB -spring.web.resources.add-mappings=true - +spring.cloud.azure.active-directory.enabled=true +spring.cloud.azure.active-directory.profile.tenant-id=${AZURE_TENANT_ID} +spring.cloud.azure.active-directory.credential.client-id=${AZURE_CLIENT_ID} +spring.cloud.azure.active-directory.credential.client-secret=${AZURE_CLIENT_SECRET} +spring.cloud.azure.active-directory.authorization-clients.graph.scopes=openid,profile,email,https://graph.microsoft.com/User.Read