From be47662c4c8bf814b55cff800a0cf0e97721fa7d Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Thu, 23 Apr 2026 08:11:12 +0200 Subject: [PATCH 1/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration (#4864) --- .../config/security/SecurityService.java | 33 ++++ .../src/main/resources/application.properties | 1 + .../config/security/SecurityServiceTest.java | 157 ++++++++++++++++++ .../src/test/resources/application.properties | 2 + 4 files changed, 193 insertions(+) create mode 100644 openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java diff --git a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java index 83617ecd5d8..0f4aa10edb3 100644 --- a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java +++ b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java @@ -2,21 +2,27 @@ import static java.util.Optional.ofNullable; import static org.springframework.util.StringUtils.hasLength; +import static org.springframework.util.StringUtils.hasText; +import io.openaev.database.model.Tenant; import io.openaev.database.model.User; import io.openaev.database.repository.UserRepository; import io.openaev.service.UserMappingService; import io.openaev.service.UserService; +import io.openaev.service.tenants.TenantService; import io.openaev.service.user_events.UserEventService; +import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotBlank; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.UUID; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.env.Environment; import org.springframework.stereotype.Service; +@Slf4j @Service @RequiredArgsConstructor public class SecurityService { @@ -26,6 +32,7 @@ public class SecurityService { public static final String GROUPS_MANAGEMENT_SUFFIX = ".groups_management"; public static final String ALL_ADMIN_PATH_SUFFIX = ".all_admin"; public static final String AUDIENCE_PATH = ".audience"; + public static final String TENANT_ID_SUFFIX = ".tenant_id"; public static final String REGISTRATION_ID = "registration_id"; private final UserRepository userRepository; @@ -33,6 +40,7 @@ public class SecurityService { private final UserMappingService userMappingService; private final Environment env; private final UserEventService userEventService; + private final TenantService tenantService; public User userManagement( String emailAttribute, @@ -60,6 +68,7 @@ public User userManagement( String.class, ""); userMappingService.mapCurrentUserWithGroup(groupsManagementObject, user, groups); + attachTenant(registrationId, user); return this.userService.saveUser(user); } else { // If user exists, update it @@ -76,6 +85,7 @@ public User userManagement( String.class, ""); userMappingService.mapCurrentUserWithGroup(groupsManagementObject, currentUser, groups); + attachTenant(registrationId, currentUser); return this.userService.saveUser(currentUser); } } @@ -91,6 +101,29 @@ public String getAudience(@NotBlank final String registrationId) { // -- PRIVATE -- + /** + * Attaches the user to the tenant configured for the given SSO provider registration. + */ + private void attachTenant(String registrationId, User user) { + String tenantId = + env.getProperty( + OPENAEV_PROVIDER_PATH_PREFIX + registrationId + TENANT_ID_SUFFIX, String.class, ""); + if (!hasText(tenantId)) { + return; + } + boolean alreadyAttached = + user.getTenants().stream().anyMatch(t -> t.getId().equals(tenantId)); + if (alreadyAttached) { + return; + } + try { + Tenant tenant = tenantService.findById(tenantId); + user.getTenants().add(tenant); + } catch (EntityNotFoundException e) { + log.warn("SSO tenant ID '{}' configured but not found in database", tenantId); + } + } + private List getAdminRoles(@NotBlank final String registrationId) { String rolesAdminConfig = OPENAEV_PROVIDER_PATH_PREFIX + registrationId + ROLES_ADMIN_PATH_SUFFIX; diff --git a/openaev-api/src/main/resources/application.properties b/openaev-api/src/main/resources/application.properties index 04198a855ba..85bf2429b4b 100644 --- a/openaev-api/src/main/resources/application.properties +++ b/openaev-api/src/main/resources/application.properties @@ -169,6 +169,7 @@ openaev.auth-saml2-enable=false # openaev.provider.{registrationId}.roles_admin= # openaev.provider.{registrationId}.audience= # openaev.provider.{registrationId}.groups_management= +# openaev.provider.{registrationId}.tenant_id= ## Kerberos openaev.auth-kerberos-enable=false diff --git a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java new file mode 100644 index 00000000000..ca3d4950dbd --- /dev/null +++ b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java @@ -0,0 +1,157 @@ +package io.openaev.config.security; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.openaev.IntegrationTest; +import io.openaev.database.model.Tenant; +import io.openaev.database.model.User; +import io.openaev.database.repository.UserRepository; +import io.openaev.utils.fixtures.UserFixture; +import io.openaev.utils.fixtures.tenants.TenantComposer; +import io.openaev.utils.fixtures.tenants.TenantFixture; +import io.openaev.utils.fixtures.composers.UserComposer; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@Transactional +class SecurityServiceTest extends IntegrationTest { + + private static final String REGISTRATION_ID = "oidc"; + private static final String SSO_EMAIL = "sso-user@integration.test"; + private static final String SSO_TENANT_ID = "sso-test-tenant-id"; + + @Autowired private SecurityService securityService; + @Autowired private UserRepository userRepository; + @Autowired private UserComposer userComposer; + @Autowired private TenantComposer tenantComposer; + @Autowired protected EntityManager entityManager; + + @BeforeEach + void setup() { + userComposer.reset(); + tenantComposer.reset(); + } + + @Nested + class UserCreation { + + @Test + void given_unknownEmail_should_createUser() { + // -- ARRANGE -- + + // -- ACT -- + User result = + securityService.userManagement( + SSO_EMAIL, REGISTRATION_ID, List.of(), List.of(), "John", "Doe"); + + // -- ASSERT -- + assertThat(result).isNotNull(); + assertThat(result.getEmail()).isEqualTo(SSO_EMAIL); + assertThat(result.getFirstname()).isEqualTo("John"); + assertThat(result.getLastname()).isEqualTo("Doe"); + + Optional persisted = userRepository.findByEmailIgnoreCase(SSO_EMAIL); + assertThat(persisted).isPresent(); + } + } + + @Nested + class UserUpdate { + + @Test + void given_existingUser_should_updateName() { + // -- ARRANGE -- + User user = UserFixture.getUser("Old", "Name", SSO_EMAIL); + userComposer.forUser(user).persist(); + entityManager.flush(); + entityManager.clear(); + + // -- ACT -- + User result = + securityService.userManagement( + SSO_EMAIL, REGISTRATION_ID, List.of(), List.of(), "New", "Name"); + + // -- ASSERT -- + assertThat(result.getFirstname()).isEqualTo("New"); + assertThat(result.getLastname()).isEqualTo("Name"); + } + + @Test + void given_emptyEmail_should_returnNull() { + // -- ARRANGE -- + + // -- ACT -- + User result = + securityService.userManagement( + "", REGISTRATION_ID, List.of(), List.of(), "John", "Doe"); + + // -- ASSERT -- + assertThat(result).isNull(); + } + } + + @Nested + class AttachTenant { + + @Test + void given_newUserWithTenantConfigured_should_attachTenant() { + // -- ARRANGE -- + Tenant tenant = TenantFixture.getTenant(); + tenant.setId(SSO_TENANT_ID); + tenantComposer.forTenant(tenant).persist(); + entityManager.flush(); + entityManager.clear(); + + // -- ACT -- + User result = + securityService.userManagement( + SSO_EMAIL, REGISTRATION_ID, List.of(), List.of(), "John", "Doe"); + + // -- ASSERT -- + assertThat(result.getTenants()).hasSize(1); + assertThat(result.getTenants().getFirst().getId()).isEqualTo(SSO_TENANT_ID); + } + + @Test + void given_existingUserWithTenantAlreadyAttached_should_notDuplicate() { + // -- ARRANGE -- + Tenant tenant = TenantFixture.getTenant(); + tenant.setId(SSO_TENANT_ID); + tenantComposer.forTenant(tenant).persist(); + User user = UserFixture.getUser("John", "Doe", SSO_EMAIL); + user.getTenants().add(tenant); + userComposer.forUser(user).persist(); + entityManager.flush(); + entityManager.clear(); + + // -- ACT -- + User result = + securityService.userManagement( + SSO_EMAIL, REGISTRATION_ID, List.of(), List.of(), "John", "Doe"); + + // -- ASSERT -- + assertThat(result.getTenants()).hasSize(1); + } + + @Test + void given_tenantNotFoundInDb_should_notError() { + // -- ARRANGE -- + // tenant_id is configured in test application.properties but tenant is not persisted + + // -- ACT -- + User result = + securityService.userManagement( + SSO_EMAIL, REGISTRATION_ID, List.of(), List.of(), "John", "Doe"); + + // -- ASSERT -- + assertThat(result).isNotNull(); + assertThat(result.getTenants()).isEmpty(); + } + } +} diff --git a/openaev-api/src/test/resources/application.properties b/openaev-api/src/test/resources/application.properties index c290fc91bd9..ac0e5f96fc5 100644 --- a/openaev-api/src/test/resources/application.properties +++ b/openaev-api/src/test/resources/application.properties @@ -68,6 +68,8 @@ openaev.cron.config.steps.delay.queue.polling.interval=10000 openaev.auth-local-enable=false ## Oauth openaev.auth-openid-enable=false +## SSO provider for tests +openaev.provider.oidc.tenant_id=sso-test-tenant-id ## Kerberos openaev.auth-kerberos-enable=false From 22d37ae7f5c3a5f5bf0193078793552643502054 Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Thu, 23 Apr 2026 08:14:55 +0200 Subject: [PATCH 2/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration (#4864) --- .../java/io/openaev/config/security/SecurityService.java | 7 ++----- .../io/openaev/config/security/SecurityServiceTest.java | 5 ++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java index 0f4aa10edb3..a8f6c310a90 100644 --- a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java +++ b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java @@ -101,9 +101,7 @@ public String getAudience(@NotBlank final String registrationId) { // -- PRIVATE -- - /** - * Attaches the user to the tenant configured for the given SSO provider registration. - */ + /** Attaches the user to the tenant configured for the given SSO provider registration. */ private void attachTenant(String registrationId, User user) { String tenantId = env.getProperty( @@ -111,8 +109,7 @@ private void attachTenant(String registrationId, User user) { if (!hasText(tenantId)) { return; } - boolean alreadyAttached = - user.getTenants().stream().anyMatch(t -> t.getId().equals(tenantId)); + boolean alreadyAttached = user.getTenants().stream().anyMatch(t -> t.getId().equals(tenantId)); if (alreadyAttached) { return; } diff --git a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java index ca3d4950dbd..7bd209b8359 100644 --- a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java +++ b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java @@ -7,9 +7,9 @@ import io.openaev.database.model.User; import io.openaev.database.repository.UserRepository; import io.openaev.utils.fixtures.UserFixture; +import io.openaev.utils.fixtures.composers.UserComposer; import io.openaev.utils.fixtures.tenants.TenantComposer; import io.openaev.utils.fixtures.tenants.TenantFixture; -import io.openaev.utils.fixtures.composers.UserComposer; import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import java.util.List; @@ -88,8 +88,7 @@ void given_emptyEmail_should_returnNull() { // -- ACT -- User result = - securityService.userManagement( - "", REGISTRATION_ID, List.of(), List.of(), "John", "Doe"); + securityService.userManagement("", REGISTRATION_ID, List.of(), List.of(), "John", "Doe"); // -- ASSERT -- assertThat(result).isNull(); From 694eae8417d8d81c555d6b6bccf3cb9208c6655b Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Tue, 28 Apr 2026 15:43:31 +0200 Subject: [PATCH 3/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration (#4864) --- .../io/openaev/config/security/SecurityServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java index 7bd209b8359..a33d3d10a2b 100644 --- a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java +++ b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java @@ -10,16 +10,17 @@ import io.openaev.utils.fixtures.composers.UserComposer; import io.openaev.utils.fixtures.tenants.TenantComposer; import io.openaev.utils.fixtures.tenants.TenantFixture; -import jakarta.persistence.EntityManager; -import jakarta.transaction.Transactional; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; @Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class SecurityServiceTest extends IntegrationTest { private static final String REGISTRATION_ID = "oidc"; @@ -30,7 +31,6 @@ class SecurityServiceTest extends IntegrationTest { @Autowired private UserRepository userRepository; @Autowired private UserComposer userComposer; @Autowired private TenantComposer tenantComposer; - @Autowired protected EntityManager entityManager; @BeforeEach void setup() { From ad599b84d0d386260f660b65e27c04e5fd04e0e0 Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Tue, 28 Apr 2026 16:33:52 +0200 Subject: [PATCH 4/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration (#4864) --- .../config/security/SecurityServiceTest.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java index a33d3d10a2b..d1503f4bb6a 100644 --- a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java +++ b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java @@ -1,5 +1,7 @@ package io.openaev.config.security; +import static io.openaev.config.security.SecurityService.OPENAEV_PROVIDER_PATH_PREFIX; +import static io.openaev.config.security.SecurityService.TENANT_ID_SUFFIX; import static org.assertj.core.api.Assertions.assertThat; import io.openaev.IntegrationTest; @@ -17,6 +19,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.transaction.annotation.Transactional; @Transactional @@ -25,17 +28,22 @@ class SecurityServiceTest extends IntegrationTest { private static final String REGISTRATION_ID = "oidc"; private static final String SSO_EMAIL = "sso-user@integration.test"; - private static final String SSO_TENANT_ID = "sso-test-tenant-id"; @Autowired private SecurityService securityService; @Autowired private UserRepository userRepository; @Autowired private UserComposer userComposer; @Autowired private TenantComposer tenantComposer; + @Autowired private Environment env; + + private String ssoTenantId; @BeforeEach void setup() { userComposer.reset(); tenantComposer.reset(); + ssoTenantId = + env.getProperty( + OPENAEV_PROVIDER_PATH_PREFIX + REGISTRATION_ID + TENANT_ID_SUFFIX, String.class, ""); } @Nested @@ -69,8 +77,6 @@ void given_existingUser_should_updateName() { // -- ARRANGE -- User user = UserFixture.getUser("Old", "Name", SSO_EMAIL); userComposer.forUser(user).persist(); - entityManager.flush(); - entityManager.clear(); // -- ACT -- User result = @@ -102,10 +108,8 @@ class AttachTenant { void given_newUserWithTenantConfigured_should_attachTenant() { // -- ARRANGE -- Tenant tenant = TenantFixture.getTenant(); - tenant.setId(SSO_TENANT_ID); + tenant.setId(ssoTenantId); tenantComposer.forTenant(tenant).persist(); - entityManager.flush(); - entityManager.clear(); // -- ACT -- User result = @@ -114,20 +118,18 @@ void given_newUserWithTenantConfigured_should_attachTenant() { // -- ASSERT -- assertThat(result.getTenants()).hasSize(1); - assertThat(result.getTenants().getFirst().getId()).isEqualTo(SSO_TENANT_ID); + assertThat(result.getTenants().getFirst().getId()).isEqualTo(ssoTenantId); } @Test void given_existingUserWithTenantAlreadyAttached_should_notDuplicate() { // -- ARRANGE -- Tenant tenant = TenantFixture.getTenant(); - tenant.setId(SSO_TENANT_ID); + tenant.setId(ssoTenantId); tenantComposer.forTenant(tenant).persist(); User user = UserFixture.getUser("John", "Doe", SSO_EMAIL); user.getTenants().add(tenant); userComposer.forUser(user).persist(); - entityManager.flush(); - entityManager.clear(); // -- ACT -- User result = From e6fd238bb4bc653dd10060437c5307f7bace1994 Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Tue, 28 Apr 2026 16:36:34 +0200 Subject: [PATCH 5/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration on groups (#4864) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix(ci): keep SecurityServiceTest unchanged (no groups_management dependency) SecurityServiceTest only reads tenant_id (simple string) from application.properties — not the JSON blob. No change needed here. fix(ci): load groups_management from JSON file in UserMappingServiceTest Replace env.getProperty() calls with file-based loading to break the CodeQL taint tracking chain that caused infinite analysis. fix(ci): remove inline JSON from test application.properties The groups_management JSON is now loaded from a dedicated file (groups-management-test.json) to avoid CodeQL taint tracking issues. fix(ci): extract groups_management JSON to dedicated test file Move inline JSON from application.properties to a dedicated JSON file to prevent CodeQL taint tracking from hanging on the env.getProperty() → ObjectMapper.readValue() → findById() data flow. --- .../openaev/service/UserMappingService.java | 30 ++++++- .../java/io/openaev/sso/GroupMapping.java | 3 + .../openaev/sso/UserMappingServiceTest.java | 79 ++++++++++++------- .../src/test/resources/application.properties | 2 +- 4 files changed, 84 insertions(+), 30 deletions(-) diff --git a/openaev-api/src/main/java/io/openaev/service/UserMappingService.java b/openaev-api/src/main/java/io/openaev/service/UserMappingService.java index 2d121809c8d..fb22222175b 100644 --- a/openaev-api/src/main/java/io/openaev/service/UserMappingService.java +++ b/openaev-api/src/main/java/io/openaev/service/UserMappingService.java @@ -5,9 +5,12 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.openaev.database.model.Group; +import io.openaev.database.model.Tenant; import io.openaev.database.model.User; import io.openaev.database.repository.GroupRepository; +import io.openaev.service.tenants.TenantService; import io.openaev.sso.GroupMapping; +import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotBlank; import java.io.IOException; import java.util.ArrayList; @@ -29,6 +32,7 @@ public class UserMappingService { private final GroupRepository groupRepository; + private final TenantService tenantService; private final Environment env; public static final String ROLES_PATH_SUFFIX = "roles_path"; public static final String GROUPS_PATH_SUFFIX = "groups_path"; @@ -64,6 +68,7 @@ public void mapCurrentUserWithGroup(String property, User user, List gro log.error("Did not create new group"); } } + attachTenantFromGroupMapping(mapping, user); } else { log.error(String.format("No corresponding group found for group %s", role)); } @@ -86,14 +91,35 @@ public void mapCurrentUserWithGroup(String property, User user, List gro private static List safeParseMappings(String json) { ObjectMapper mapper = new ObjectMapper(); try { - return mapper.readValue(json, new TypeReference>() {}); + return mapper.readValue(json, new TypeReference<>() {}); } catch (IOException e) { // Log and return empty list instead of throwing - System.err.println("Failed to parse mappings: " + e.getMessage()); + log.error("Failed to parse group mappings: {}", e.getMessage(), e); return List.of(); } } + /** + * Attaches the user to the tenant configured in the group mapping, if any. Skips if tenantId is + * not set, the user is already attached, or the tenant is not found. + */ + private void attachTenantFromGroupMapping(GroupMapping mapping, User user) { + String tenantId = mapping.getTenantId(); + if (tenantId == null || tenantId.isBlank()) { + return; + } + boolean alreadyAttached = user.getTenants().stream().anyMatch(t -> t.getId().equals(tenantId)); + if (alreadyAttached) { + return; + } + try { + Tenant tenant = tenantService.findById(tenantId); + user.getTenants().add(tenant); + } catch (EntityNotFoundException e) { + log.warn("Group mapping tenant ID '{}' configured but not found in database", tenantId); + } + } + /** * Extract the roles from a user * diff --git a/openaev-api/src/main/java/io/openaev/sso/GroupMapping.java b/openaev-api/src/main/java/io/openaev/sso/GroupMapping.java index 1105c925f47..22b71f98981 100644 --- a/openaev-api/src/main/java/io/openaev/sso/GroupMapping.java +++ b/openaev-api/src/main/java/io/openaev/sso/GroupMapping.java @@ -14,4 +14,7 @@ public class GroupMapping { @JsonProperty("autoCreate") private boolean autoCreate; + + @JsonProperty("tenantId") + private String tenantId; } diff --git a/openaev-api/src/test/java/io/openaev/sso/UserMappingServiceTest.java b/openaev-api/src/test/java/io/openaev/sso/UserMappingServiceTest.java index 38a9defd678..853bc74bb50 100644 --- a/openaev-api/src/test/java/io/openaev/sso/UserMappingServiceTest.java +++ b/openaev-api/src/test/java/io/openaev/sso/UserMappingServiceTest.java @@ -7,6 +7,7 @@ import io.openaev.IntegrationTest; import io.openaev.database.model.Group; +import io.openaev.database.model.Tenant; import io.openaev.database.model.User; import io.openaev.opencti.connectors.Constants; import io.openaev.service.UserMappingService; @@ -14,6 +15,8 @@ import io.openaev.utils.fixtures.UserFixture; import io.openaev.utils.fixtures.composers.GroupComposer; import io.openaev.utils.fixtures.composers.UserComposer; +import io.openaev.utils.fixtures.tenants.TenantComposer; +import io.openaev.utils.fixtures.tenants.TenantFixture; import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import java.util.*; @@ -32,27 +35,42 @@ import org.springframework.test.util.ReflectionTestUtils; @Transactional -public class UserMappingServiceTest extends IntegrationTest { +class UserMappingServiceTest extends IntegrationTest { + + private static final String GROUP_TENANT_ID = "group-tenant-id"; @Autowired private GroupComposer groupComposer; @Autowired UserComposer userComposer; + @Autowired private TenantComposer tenantComposer; @Autowired private UserMappingService userMappingService; @Autowired protected EntityManager entityManager; + private static final String GROUPS_MANAGEMENT_JSON = + """ + [ + {"idpGroup": "team-alpha", "userGroup": "Alpha", "autoCreate": true, "tenantId": "group-tenant-id"}, + {"idpGroup": "team-beta", "userGroup": "Beta", "autoCreate": true} + ] + """; + @BeforeEach - public void setup() { + void setup() { groupComposer.reset(); } @Test @DisplayName( - "When the specific group already exists and the autocreate is false, add it to the user") - public void whenTheSpecificGroupAlreadyExistsAndTheAutocreateIsFalse_addItToTheUser() { + "When the specific group already exists and the autocreate is false, add it to the user and" + + " attach tenant") + void whenTheSpecificGroupAlreadyExistsAndTheAutocreateIsFalse_addItToTheUser() { // -- ARRANGE --- - String object = - "[{\"idpGroup\": \"observer\",\"userGroup\": \"observerUserGroup\",\"autoCreate\": \"false\"}]"; - Group specificGroup = GroupFixture.createGroupWithName("observerUserGroup"); + Tenant tenant = TenantFixture.getTenant(); + tenant.setId(GROUP_TENANT_ID); + tenantComposer.forTenant(tenant).persist(); + + String groupsManagement = GROUPS_MANAGEMENT_JSON; + Group specificGroup = GroupFixture.createGroupWithName("Alpha"); specificGroup.setId(Constants.PROCESS_STIX_GROUP_ID); specificGroup.setDescription("a description"); specificGroup.setRoles(new ArrayList<>()); @@ -63,40 +81,43 @@ public void whenTheSpecificGroupAlreadyExistsAndTheAutocreateIsFalse_addItToTheU userComposer.forUser(user).persist(); entityManager.flush(); entityManager.clear(); - List roles = List.of("observer"); + List roles = List.of("team-alpha"); // ---- ACT ---- - userMappingService.mapCurrentUserWithGroup(object, user, roles); + userMappingService.mapCurrentUserWithGroup(groupsManagement, user, roles); // -- ASSERT -- assertTrue(user.getGroups().contains(specificGroup)); + assertThat(user.getTenants().stream().anyMatch(t -> t.getId().equals(GROUP_TENANT_ID))) + .isTrue(); } @Test @DisplayName( - "When the specific group does not exist and the autocreate is true, create it and add it to the user") - public void whenTheSpecificGroupDoesNotExistAndTheAutocreateIsTrue_createItAndAddItToTheUser() { + "When the specific group does not exist and the autocreate is true, create it and add it to" + + " the user") + void whenTheSpecificGroupDoesNotExistAndTheAutocreateIsTrue_createItAndAddItToTheUser() { // -- ARRANGE --- - String object = - "[{\"idpGroup\": \"observer\",\"userGroup\": \"admin\",\"autoCreate\": \"true\"}]"; + String groupsManagement = GROUPS_MANAGEMENT_JSON; User user = UserFixture.getUser(); userComposer.forUser(user).persist(); entityManager.flush(); entityManager.clear(); - List roles = List.of("observer"); + List roles = List.of("team-beta"); // ---- ACT ---- - userMappingService.mapCurrentUserWithGroup(object, user, roles); + userMappingService.mapCurrentUserWithGroup(groupsManagement, user, roles); // -- ASSERT -- Group userGroup = user.getGroups().get(0); - assertTrue(userGroup.getName().equals("admin")); + assertTrue(userGroup.getName().equals("Beta")); + assertThat(user.getTenants().isEmpty()).isTrue(); } @Test @DisplayName("When the specific group does not exist and the autocreate is false, do nothing") - public void whenTheSpecificGroupDoesNotExistAndTheAutocreateIsFalse_doNothing() { + void whenTheSpecificGroupDoesNotExistAndTheAutocreateIsFalse_doNothing() { // -- ARRANGE --- String object = @@ -116,7 +137,7 @@ public void whenTheSpecificGroupDoesNotExistAndTheAutocreateIsFalse_doNothing() @Test @DisplayName("When group from idp and group from oaev do not match, do nothing") - public void whenGroupFromIdpAndRolesFromOaevDoNotMatch_doNothing() { + void whenGroupFromIdpAndRolesFromOaevDoNotMatch_doNothing() { // -- ARRANGE --- String object = @@ -143,11 +164,13 @@ public void whenGroupFromIdpAndRolesFromOaevDoNotMatch_doNothing() { @Test @DisplayName("When multiple config is set, act accordingly") - public void whenMultipleConfigIsSet_actAccordingly() { + void whenMultipleConfigIsSet_actAccordingly() { // -- ARRANGE --- String object = - "[{\"idpGroup\": \"observer\",\"userGroup\": \"admin1\",\"autoCreate\": \"false\"},{\"idpGroup\": \"observer\",\"userGroup\": \"admin2\",\"autoCreate\": \"true\"}]"; + "[{\"idpGroup\": \"observer\",\"userGroup\": \"admin1\",\"autoCreate\":" + + " \"false\"},{\"idpGroup\": \"observer\",\"userGroup\": \"admin2\",\"autoCreate\":" + + " \"true\"}]"; Group specificGroup = GroupFixture.createGroupWithName("observer"); specificGroup.setId(Constants.PROCESS_STIX_GROUP_ID); specificGroup.setDescription("a description"); @@ -171,11 +194,13 @@ public void whenMultipleConfigIsSet_actAccordingly() { @Test @DisplayName("When removed from the idp group, remove from oaev group") - public void whenRemovedFromIdpGroup_propagateDeleteFromGroup() { + void whenRemovedFromIdpGroup_propagateDeleteFromGroup() { // -- ARRANGE --- String object = - "[{\"idpGroup\": \"observer1\",\"userGroup\": \"observerOAEV1\",\"autoCreate\": \"true\"},{\"idpGroup\": \"observer2\",\"userGroup\": \"observerOAEV2\",\"autoCreate\": \"true\"}]"; + "[{\"idpGroup\": \"observer1\",\"userGroup\": \"observerOAEV1\",\"autoCreate\":" + + " \"true\"},{\"idpGroup\": \"observer2\",\"userGroup\":" + + " \"observerOAEV2\",\"autoCreate\": \"true\"}]"; Group specificGroup1 = GroupFixture.createGroupWithName("observerOAEV1"); specificGroup1.setId(Constants.PROCESS_STIX_GROUP_ID); specificGroup1.setDescription("a description"); @@ -207,7 +232,7 @@ public void whenRemovedFromIdpGroup_propagateDeleteFromGroup() { class TestRolesAndGroupsExtraction { @Test @DisplayName("When oidc user, extract roles accordingly") - public void whenOidcUser_extractRoles() { + void whenOidcUser_extractRoles() { // -- ARRANGE --- Environment env = Mockito.mock(Environment.class); @@ -244,7 +269,7 @@ public String getName() { @Test @DisplayName("When oidc user, extract groups accordingly") - public void whenOidcUser_extractGroups() { + void whenOidcUser_extractGroups() { // -- ARRANGE --- Environment env = Mockito.mock(Environment.class); @@ -282,7 +307,7 @@ public String getName() { @Test @DisplayName("When saml user, extract roles accordingly") - public void whenSamlUser_extractRoles() { + void whenSamlUser_extractRoles() { // -- ARRANGE --- Environment env = Mockito.mock(Environment.class); @@ -314,7 +339,7 @@ public Map> getAttributes() { @Test @DisplayName("When saml user, extract groups accordingly") - public void whenSamlUser_extractGroups() { + void whenSamlUser_extractGroups() { // -- ARRANGE --- Environment env = Mockito.mock(Environment.class); @@ -347,7 +372,7 @@ public Map> getAttributes() { @Test @DisplayName("When not implemented user, throw exception") - public void whenNotImplementedUser_throwException() { + void whenNotImplementedUser_throwException() { // -- ARRANGE --- Environment env = Mockito.mock(Environment.class); diff --git a/openaev-api/src/test/resources/application.properties b/openaev-api/src/test/resources/application.properties index ac0e5f96fc5..86dc9902f80 100644 --- a/openaev-api/src/test/resources/application.properties +++ b/openaev-api/src/test/resources/application.properties @@ -200,4 +200,4 @@ spring.datasource.hikari.maximum-pool-size=5 # Help performances on slow compute spring.datasource.hikari.minimum-idle=1 -openaev.enabled-dev-features=SENTINEL_ONE_EXECUTOR,PALO_ALTO_CORTEX_EXECUTOR,INJECT_CHAINING \ No newline at end of file +openaev.enabled-dev-features=SENTINEL_ONE_EXECUTOR,PALO_ALTO_CORTEX_EXECUTOR,INJECT_CHAINING From 510cf507797179892f3c616e08dbe9020d6ce366 Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Wed, 29 Apr 2026 16:21:36 +0200 Subject: [PATCH 6/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration on groups (#4864) --- .../config/security/SecurityServiceTest.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java index d1503f4bb6a..ab2850c10e6 100644 --- a/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java +++ b/openaev-api/src/test/java/io/openaev/config/security/SecurityServiceTest.java @@ -1,7 +1,5 @@ package io.openaev.config.security; -import static io.openaev.config.security.SecurityService.OPENAEV_PROVIDER_PATH_PREFIX; -import static io.openaev.config.security.SecurityService.TENANT_ID_SUFFIX; import static org.assertj.core.api.Assertions.assertThat; import io.openaev.IntegrationTest; @@ -19,7 +17,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; import org.springframework.transaction.annotation.Transactional; @Transactional @@ -28,22 +25,17 @@ class SecurityServiceTest extends IntegrationTest { private static final String REGISTRATION_ID = "oidc"; private static final String SSO_EMAIL = "sso-user@integration.test"; + private static final String SSO_TENANT_ID = "sso-test-tenant-id"; @Autowired private SecurityService securityService; @Autowired private UserRepository userRepository; @Autowired private UserComposer userComposer; @Autowired private TenantComposer tenantComposer; - @Autowired private Environment env; - - private String ssoTenantId; @BeforeEach void setup() { userComposer.reset(); tenantComposer.reset(); - ssoTenantId = - env.getProperty( - OPENAEV_PROVIDER_PATH_PREFIX + REGISTRATION_ID + TENANT_ID_SUFFIX, String.class, ""); } @Nested @@ -108,7 +100,7 @@ class AttachTenant { void given_newUserWithTenantConfigured_should_attachTenant() { // -- ARRANGE -- Tenant tenant = TenantFixture.getTenant(); - tenant.setId(ssoTenantId); + tenant.setId(SSO_TENANT_ID); tenantComposer.forTenant(tenant).persist(); // -- ACT -- @@ -118,14 +110,14 @@ void given_newUserWithTenantConfigured_should_attachTenant() { // -- ASSERT -- assertThat(result.getTenants()).hasSize(1); - assertThat(result.getTenants().getFirst().getId()).isEqualTo(ssoTenantId); + assertThat(result.getTenants().getFirst().getId()).isEqualTo(SSO_TENANT_ID); } @Test void given_existingUserWithTenantAlreadyAttached_should_notDuplicate() { // -- ARRANGE -- Tenant tenant = TenantFixture.getTenant(); - tenant.setId(ssoTenantId); + tenant.setId(SSO_TENANT_ID); tenantComposer.forTenant(tenant).persist(); User user = UserFixture.getUser("John", "Doe", SSO_EMAIL); user.getTenants().add(tenant); From b50f447196e8fdb216beed0a730d35d12f08aa7b Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Thu, 30 Apr 2026 14:16:57 +0200 Subject: [PATCH 7/9] Reput everything --- .../io/openaev/config/security/SecurityService.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java index a8f6c310a90..15f7c631ab1 100644 --- a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java +++ b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java @@ -6,10 +6,10 @@ import io.openaev.database.model.Tenant; import io.openaev.database.model.User; +import io.openaev.database.repository.TenantRepository; import io.openaev.database.repository.UserRepository; import io.openaev.service.UserMappingService; import io.openaev.service.UserService; -import io.openaev.service.tenants.TenantService; import io.openaev.service.user_events.UserEventService; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotBlank; @@ -40,7 +40,7 @@ public class SecurityService { private final UserMappingService userMappingService; private final Environment env; private final UserEventService userEventService; - private final TenantService tenantService; + private final TenantRepository tenantRepository; public User userManagement( String emailAttribute, @@ -114,7 +114,11 @@ private void attachTenant(String registrationId, User user) { return; } try { - Tenant tenant = tenantService.findById(tenantId); + Tenant tenant = + tenantRepository + .findById(tenantId) + .orElseThrow(() -> new EntityNotFoundException("Tenant not found: " + tenantId)); + ; user.getTenants().add(tenant); } catch (EntityNotFoundException e) { log.warn("SSO tenant ID '{}' configured but not found in database", tenantId); From c8690fe944aa599a3859713c6d56335a3f30c2b9 Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Thu, 30 Apr 2026 19:11:08 +0200 Subject: [PATCH 8/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration on groups (#4864) --- .../java/io/openaev/config/security/SecurityService.java | 1 - .../main/java/io/openaev/service/UserMappingService.java | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java index 15f7c631ab1..5c3ed4c18af 100644 --- a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java +++ b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java @@ -118,7 +118,6 @@ private void attachTenant(String registrationId, User user) { tenantRepository .findById(tenantId) .orElseThrow(() -> new EntityNotFoundException("Tenant not found: " + tenantId)); - ; user.getTenants().add(tenant); } catch (EntityNotFoundException e) { log.warn("SSO tenant ID '{}' configured but not found in database", tenantId); diff --git a/openaev-api/src/main/java/io/openaev/service/UserMappingService.java b/openaev-api/src/main/java/io/openaev/service/UserMappingService.java index fb22222175b..124afda0366 100644 --- a/openaev-api/src/main/java/io/openaev/service/UserMappingService.java +++ b/openaev-api/src/main/java/io/openaev/service/UserMappingService.java @@ -8,7 +8,7 @@ import io.openaev.database.model.Tenant; import io.openaev.database.model.User; import io.openaev.database.repository.GroupRepository; -import io.openaev.service.tenants.TenantService; +import io.openaev.database.repository.TenantRepository; import io.openaev.sso.GroupMapping; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotBlank; @@ -32,7 +32,7 @@ public class UserMappingService { private final GroupRepository groupRepository; - private final TenantService tenantService; + private final TenantRepository tenantRepository; private final Environment env; public static final String ROLES_PATH_SUFFIX = "roles_path"; public static final String GROUPS_PATH_SUFFIX = "groups_path"; @@ -113,7 +113,10 @@ private void attachTenantFromGroupMapping(GroupMapping mapping, User user) { return; } try { - Tenant tenant = tenantService.findById(tenantId); + Tenant tenant = + tenantRepository + .findById(tenantId) + .orElseThrow(() -> new EntityNotFoundException("Tenant not found: " + tenantId)); user.getTenants().add(tenant); } catch (EntityNotFoundException e) { log.warn("Group mapping tenant ID '{}' configured but not found in database", tenantId); From 4dc2b8fe5d97b1b26afb6b4734ac7855d92e5513 Mon Sep 17 00:00:00 2001 From: Romuald Lemesle Date: Thu, 30 Apr 2026 21:36:31 +0200 Subject: [PATCH 9/9] [backend] feat(multi-tenancy): add tenant thanks to sso configuration on groups (#4864) --- .../io/openaev/config/security/SecurityService.java | 12 ++++-------- .../java/io/openaev/service/UserMappingService.java | 12 ++++-------- .../database/repository/TenantRepository.java | 4 ++-- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java index 5c3ed4c18af..78376543027 100644 --- a/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java +++ b/openaev-api/src/main/java/io/openaev/config/security/SecurityService.java @@ -11,7 +11,6 @@ import io.openaev.service.UserMappingService; import io.openaev.service.UserService; import io.openaev.service.user_events.UserEventService; -import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotBlank; import java.util.ArrayList; import java.util.List; @@ -113,15 +112,12 @@ private void attachTenant(String registrationId, User user) { if (alreadyAttached) { return; } - try { - Tenant tenant = - tenantRepository - .findById(tenantId) - .orElseThrow(() -> new EntityNotFoundException("Tenant not found: " + tenantId)); - user.getTenants().add(tenant); - } catch (EntityNotFoundException e) { + if (!tenantRepository.existsById(tenantId)) { log.warn("SSO tenant ID '{}' configured but not found in database", tenantId); + return; } + Tenant tenant = tenantRepository.getReferenceById(tenantId); + user.getTenants().add(tenant); } private List getAdminRoles(@NotBlank final String registrationId) { diff --git a/openaev-api/src/main/java/io/openaev/service/UserMappingService.java b/openaev-api/src/main/java/io/openaev/service/UserMappingService.java index 124afda0366..b6b47b0cba2 100644 --- a/openaev-api/src/main/java/io/openaev/service/UserMappingService.java +++ b/openaev-api/src/main/java/io/openaev/service/UserMappingService.java @@ -10,7 +10,6 @@ import io.openaev.database.repository.GroupRepository; import io.openaev.database.repository.TenantRepository; import io.openaev.sso.GroupMapping; -import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotBlank; import java.io.IOException; import java.util.ArrayList; @@ -112,15 +111,12 @@ private void attachTenantFromGroupMapping(GroupMapping mapping, User user) { if (alreadyAttached) { return; } - try { - Tenant tenant = - tenantRepository - .findById(tenantId) - .orElseThrow(() -> new EntityNotFoundException("Tenant not found: " + tenantId)); - user.getTenants().add(tenant); - } catch (EntityNotFoundException e) { + if (!tenantRepository.existsById(tenantId)) { log.warn("Group mapping tenant ID '{}' configured but not found in database", tenantId); + return; } + Tenant tenant = tenantRepository.getReferenceById(tenantId); + user.getTenants().add(tenant); } /** diff --git a/openaev-model/src/main/java/io/openaev/database/repository/TenantRepository.java b/openaev-model/src/main/java/io/openaev/database/repository/TenantRepository.java index 152fca9cdd4..6741bf4ca75 100644 --- a/openaev-model/src/main/java/io/openaev/database/repository/TenantRepository.java +++ b/openaev-model/src/main/java/io/openaev/database/repository/TenantRepository.java @@ -3,16 +3,16 @@ import io.openaev.database.model.Tenant; import java.time.Instant; import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface TenantRepository - extends CrudRepository, JpaSpecificationExecutor { + extends JpaRepository, JpaSpecificationExecutor { // -- READ --