- Integrated entra id into "prod"-profile

This commit is contained in:
Jan 2025-10-02 21:46:38 +02:00
parent f6160a4153
commit 10687ffe5d
8 changed files with 207 additions and 28 deletions

View file

@ -60,6 +60,10 @@
<groupId>com.azure.spring</groupId> <groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-jdbc-mysql</artifactId> <artifactId>spring-cloud-azure-starter-jdbc-mysql</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>

View file

@ -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);
}
}
}

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.config;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@ -10,6 +11,7 @@ import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException; import java.io.IOException;
@Configuration @Configuration
@Profile("!dev & !test")
public class FrontendConfig implements WebMvcConfigurer { public class FrontendConfig implements WebMvcConfigurer {
@Override @Override

View file

@ -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<? extends GrantedAuthority> authorities,
OidcIdToken idToken,
OidcUserInfo userInfo,
String nameAttributeKey,
Integer userId) {
super(authorities, idToken, userInfo, nameAttributeKey);
this.userId = userId;
}
public Integer getSqlUserId() {
return userId;
}
}

View file

@ -1,26 +1,39 @@
package de.avatic.lcc.config; 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.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 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.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.HashSet;
import java.util.List; import java.util.Set;
@Configuration @Configuration
public class SecurityConfig { public class SecurityConfig {
@Bean @Bean
@Profile("!dev & !test") // Only active when NOT in dev profile @Profile("!dev & !test") // Only active when NOT in dev profile
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 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(); return http.build();
} }
@ -32,4 +45,54 @@ public class SecurityConfig {
.csrf(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable)
.build(); .build();
} }
@Bean
@Profile("!dev & !test")
public OAuth2UserService<OidcUserRequest, OidcUser> 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<GrantedAuthority> 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
);
};
}
} }

View file

@ -5,7 +5,6 @@ import de.avatic.lcc.model.users.User;
import de.avatic.lcc.repositories.pagination.SearchQueryPagination; import de.avatic.lcc.repositories.pagination.SearchQueryPagination;
import de.avatic.lcc.repositories.pagination.SearchQueryResult; import de.avatic.lcc.repositories.pagination.SearchQueryResult;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.KeyHolder;
@ -19,7 +18,6 @@ import java.sql.Statement;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
@Repository @Repository
public class UserRepository { public class UserRepository {
@ -69,7 +67,7 @@ public class UserRepository {
List<Integer> groupIds = findGroupIds(user.getGroups().stream().map(Group::getName).toList()); List<Integer> groupIds = findGroupIds(user.getGroups().stream().map(Group::getName).toList());
if(userId == null) { if (userId == null) {
KeyHolder keyHolder = new GeneratedKeyHolder(); KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> { jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement( PreparedStatement ps = connection.prepareStatement(
@ -86,15 +84,14 @@ public class UserRepository {
}, keyHolder); }, keyHolder);
userId = Objects.requireNonNull(keyHolder.getKey()).intValue(); userId = Objects.requireNonNull(keyHolder.getKey()).intValue();
} } else {
else {
String query = """ 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); 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 FROM sys_user
WHERE id = ?"""; 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<User> { private class UserMapper implements RowMapper<User> {
@Override @Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException { 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.setId(id);
user.setActive(rs.getBoolean("is_active")); user.setActive(rs.getBoolean("is_active"));
user.setEmail(rs.getString("email")); user.setEmail(rs.getString("email"));
user.setFirstName(rs.getString("firstname")); user.setFirstName(rs.getString("firstname"));
user.setLastName(rs.getString("lastname")); user.setLastName(rs.getString("lastname"));
user.setWorkdayId(rs.getString("workday_id")); user.setWorkdayId(rs.getString("workday_id"));
user.setGroups(getGroupMemberships(id)); user.setGroups(getGroupMemberships(id));
return user; return user;
} }
} }

View file

@ -1,5 +1,6 @@
package de.avatic.lcc.service.access; 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.CalculationStatus;
import de.avatic.lcc.dto.calculation.PremiseDTO; import de.avatic.lcc.dto.calculation.PremiseDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO; 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 de.avatic.lcc.util.exception.base.InternalErrorException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -84,6 +88,14 @@ public class PremisesService {
//TODO use actual user. //TODO use actual user.
userId = 1; 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); return SearchQueryResult.map(premiseRepository.listPremises(filter, new SearchQueryPagination(page, limit), userId, deleted, archived, done), admin ? premiseTransformer::toPremiseDTOWithUserInfo : premiseTransformer::toPremiseDTO);
} }

View file

@ -5,7 +5,6 @@ spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD} spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.sql.init.mode=never spring.sql.init.mode=never
#spring.profiles.active=setup
lcc.bulk.sheet_password=secretSheet?! lcc.bulk.sheet_password=secretSheet?!
lcc.allowed_cors=${ALLOWED_CORS_DOMAIN} lcc.allowed_cors=${ALLOWED_CORS_DOMAIN}
azure.maps.subscription.key=${AZURE_MAPS_SUBSCRIPTION_KEY} 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 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-file-size=30MB
spring.servlet.multipart.max-request-size=50MB 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