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 extends GrantedAuthority> 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