Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

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.TenantRepository;
import io.openaev.database.repository.UserRepository;
import io.openaev.service.UserMappingService;
import io.openaev.service.UserService;
Expand All @@ -14,9 +17,11 @@
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 {
Expand All @@ -26,13 +31,15 @@ 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;
private final UserService userService;
private final UserMappingService userMappingService;
private final Environment env;
private final UserEventService userEventService;
private final TenantRepository tenantRepository;

public User userManagement(
String emailAttribute,
Expand Down Expand Up @@ -60,6 +67,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
Expand All @@ -76,6 +84,7 @@ public User userManagement(
String.class,
"");
userMappingService.mapCurrentUserWithGroup(groupsManagementObject, currentUser, groups);
attachTenant(registrationId, currentUser);
return this.userService.saveUser(currentUser);
}
}
Expand All @@ -91,6 +100,26 @@ 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;
}
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<String> getAdminRoles(@NotBlank final String registrationId) {
String rolesAdminConfig =
OPENAEV_PROVIDER_PATH_PREFIX + registrationId + ROLES_ADMIN_PATH_SUFFIX;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
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.database.repository.TenantRepository;
import io.openaev.sso.GroupMapping;
import jakarta.validation.constraints.NotBlank;
import java.io.IOException;
Expand All @@ -29,6 +31,7 @@
public class UserMappingService {

private final GroupRepository groupRepository;
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";
Expand Down Expand Up @@ -64,6 +67,7 @@ public void mapCurrentUserWithGroup(String property, User user, List<String> gro
log.error("Did not create new group");
}
}
attachTenantFromGroupMapping(mapping, user);
} else {
log.error(String.format("No corresponding group found for group %s", role));
}
Expand All @@ -86,14 +90,35 @@ public void mapCurrentUserWithGroup(String property, User user, List<String> gro
private static List<GroupMapping> safeParseMappings(String json) {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readValue(json, new TypeReference<List<GroupMapping>>() {});
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;
}
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);
}

/**
* Extract the roles from a user
*
Expand Down
3 changes: 3 additions & 0 deletions openaev-api/src/main/java/io/openaev/sso/GroupMapping.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ public class GroupMapping {

@JsonProperty("autoCreate")
private boolean autoCreate;

@JsonProperty("tenantId")
private String tenantId;
}
1 change: 1 addition & 0 deletions openaev-api/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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.composers.UserComposer;
import io.openaev.utils.fixtures.tenants.TenantComposer;
import io.openaev.utils.fixtures.tenants.TenantFixture;
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";
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;

@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<User> 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();

// -- 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();

// -- 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();

// -- 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();
}
}
}
Loading
Loading