Skip to content
Open
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 @@ -237,4 +237,8 @@ private Profile resolveUserProfile(@NonNull List<String> roles) {
return profileMappings.resolveHighestProfileFromRoleNames(roles);
}

public List<String> getRootRolesForUser(@NonNull CanonicalUser user) {
return user.getRoles();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,10 @@ interface GroupSynchronizer {
* {@link Group} and and external {@link GroupLink link} in the process.
*/
Privileges resolvePrivilegesFor(CanonicalUser user);

/**
* Returns the names of the root roles (non group-specific) assigned to the given user in the
* external system.
*/
List<String> getRootRolesForUser(@NonNull CanonicalUser user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class GroupSynchronizerProxy implements GroupSynchronizer {

private @Autowired OrgsBasedGroupSynchronizer orgsSynchronizer;
private @Autowired RolesBasedGroupSynchronizer rolesSynchronizer;
private @Autowired RolePerOrgBasedGroupSynchronizer rolePerOrgSynchronizer;

private GroupSynchronizer resolve() {
final GroupSyncMode syncMode = props.getSyncMode();
Expand All @@ -47,6 +48,8 @@ private GroupSynchronizer resolve() {
return orgsSynchronizer;
case roles:
return rolesSynchronizer;
case role_per_org:
return rolePerOrgSynchronizer;
default:
throw new IllegalStateException("Invalid sync mode: " + syncMode);
}
Expand Down Expand Up @@ -79,4 +82,8 @@ private GroupSynchronizer resolve() {
public @Override Privileges resolvePrivilegesFor(CanonicalUser user) {
return resolve().resolvePrivilegesFor(user);
}

public @Override List<String> getRootRolesForUser(CanonicalUser user) {
return resolve().getRootRolesForUser(user);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public class IntegrationConfiguration {
return new OrgsBasedGroupSynchronizer(canonicalAccountsRepository);
}

protected @Bean RolePerOrgBasedGroupSynchronizer rolePerOrgBasedGroupSynchronizer() {
return new RolePerOrgBasedGroupSynchronizer(canonicalAccountsRepository);
}

protected @Bean LogoUpdater logoUpdater() {
return new LogoUpdater(canonicalAccountsRepository);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (C) 2009-2025 by the geOrchestra PSC
*
* This file is part of geOrchestra.
*
* geOrchestra is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* geOrchestra is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* geOrchestra. If not, see <http://www.gnu.org/licenses/>.
*/
package org.geonetwork.security.external.integration;

import org.fao.geonet.domain.Group;
import org.fao.geonet.domain.Profile;
import org.geonetwork.security.external.configuration.ExternalizedSecurityProperties;
import org.geonetwork.security.external.model.CanonicalGroup;
import org.geonetwork.security.external.model.CanonicalUser;
import org.geonetwork.security.external.model.GroupLink;
import org.geonetwork.security.external.model.GroupSyncMode;
import org.geonetwork.security.external.repository.CanonicalAccountsRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class RolePerOrgBasedGroupSynchronizer extends AbstractGroupSynchronizer {

public static final Logger log = LoggerFactory.getLogger(RolePerOrgBasedGroupSynchronizer.class.getPackage().getName());

private static final String separator = ":";

@Autowired
public ExternalizedSecurityProperties config;

public RolePerOrgBasedGroupSynchronizer(CanonicalAccountsRepository canonicalAccounts) {
super(canonicalAccounts);
}

protected @Override GroupSyncMode getOrigin() {
return GroupSyncMode.role_per_org;
}

public @Override List<CanonicalGroup> fetchCanonicalGroups() {
return canonicalAccounts.findAllOrganizations();
}

protected @Override List<CanonicalGroup> resolveGroupsOf(CanonicalUser user) {
final String orgName = user.getOrganization();
if (!StringUtils.hasLength(orgName)) {
return Collections.emptyList();
}
Stream<String> groupsName = userRoles(user)
.map(r -> r.contains(separator) ? r.split(separator)[0] : orgName
).distinct();
Stream<CanonicalGroup> roleGroups = groupsName.map(role -> this.externalGroupLinks.findByName(role)//
.map(GroupLink::getCanonical)
.orElseThrow(notFound(role)));
return roleGroups.collect(Collectors.toList());
}

@Override
public Privileges resolvePrivilegesFor(CanonicalUser user) {
final List<CanonicalGroup> canonicalGroups = resolveGroupsOf(user);

Privileges userPrivileges = new Privileges(resolveDefaultProfile(user));
Stream<Group> groups = canonicalGroups.stream().map(this::synchronize).map(GroupLink::getGeonetworkGroup);
groups.map(g -> resolvePrivilegeFor(user, g))
.forEach(userPrivileges.getAdditionalProvileges()::add);
return userPrivileges;
}

@Override
protected Profile resolveDefaultProfile(CanonicalUser user) {
return configProperties.getProfiles().resolveHighestProfileFromRoleNames(getRootRolesForUser(user));
}

private Stream<String> userRoles(CanonicalUser user) {
return user.getRoles().stream()
.filter(r -> Pattern.compile(".+" + separator + ".+").matcher(r).matches() || config.getProfiles().getRolemappings().keySet().contains(r));
}

private Privilege resolvePrivilegeFor(CanonicalUser user, Group group) {
String groupPrefix = group.getName() + separator;
List<String> rolesForGroup = userRoles(user)
.filter(r -> r.startsWith(groupPrefix) || config.getProfiles().getRolemappings().keySet().contains(r)) //e.g filter roles for this group PSC:GN_REVIEWER and GN_EDITOR
.map(this::getRootRole) // e.g get only the role part GN_REVIEWER
.collect(Collectors.toList());
Profile p = config.getProfiles().resolveHighestProfileFromRoleNames(rolesForGroup); // resolve highest profile for the roles filtered, here GN_REVIEWER
return new Privilege(group, p);
}

private Supplier<? extends IllegalArgumentException> notFound(final String orgName) {
return () -> new IllegalArgumentException(
"Organization with name '" + orgName + "' not found in internal nor external repository");
}

@Override
public List<String> getRootRolesForUser(CanonicalUser user) {
// Not used in RolePerOrg mode
return userRoles(user).map(this::getRootRole).collect(Collectors.toList());
}

private String getRootRole(String role) {
if (role.contains(separator)) {
return role.split(separator)[1];
}
return role;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@
* external system's Organizations or Roles.
*/
public enum GroupSyncMode {
orgs, roles;
orgs, roles, role_per_org;
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ public void setRolesSyncMode() {
configProps.setSyncMode(GroupSyncMode.roles);
}

public void setRolePerOrgSyncMode() {
configProps.setSyncMode(GroupSyncMode.role_per_org);
}

public ProfileMappingProperties getProfileMappings() {
return configProps.getProfiles();
}
Expand Down Expand Up @@ -136,7 +140,7 @@ public void assertUser(CanonicalUser expected, User user) {
assertEquals(expectedTitle, user.getKind());

ProfileMappingProperties profileMappings = configProps.getProfiles();
Profile expectedProfile = profileMappings.resolveHighestProfileFromRoleNames(expected.getRoles());
Profile expectedProfile = profileMappings.resolveHighestProfileFromRoleNames(groupSynchronizer.getRootRolesForUser(expected));
assertEquals(expectedProfile, user.getProfile());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// java
package org.geonetwork.security.external.integration;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;

import javax.transaction.Transactional;

import org.assertj.core.util.Sets;
import org.fao.geonet.domain.Group;
import org.fao.geonet.domain.User;
import org.geonetwork.security.external.configuration.ExternalizedSecurityProperties;
import org.geonetwork.security.external.model.CanonicalGroup;
import org.geonetwork.security.external.model.CanonicalUser;
import org.geonetwork.security.external.model.GroupLink;
import org.geonetwork.security.external.model.UserLink;
import org.junit.Before;
import org.junit.Test;
import org.springframework.test.annotation.DirtiesContext;

@Transactional
@DirtiesContext
public class RolePerOrgBasedSynchronizationIT extends AbstractAccountsReconcilingServiceIntegrationTest {

@Before
public void setUp_SetSyncModeToRolePerOrg() {
support.setRolePerOrgSyncMode();
}


@Test
public void Synchronize_on_empty_geonetwork_db_creates_all_users_and_groups_from_orgs() {
List<CanonicalUser> users = super.defaultUsers;
List<CanonicalGroup> orgs = super.defaultGroups;

assertEquals(0, support.gnUserRepository.count());
assertEquals(0, support.gnGroupRepository.count());

service.synchronize();
verify(users, orgs);
}

@Test
public void RolePerOrg_user_with_prefixed_role_maps_to_org_group() {
List<CanonicalGroup> orgGroups = super.defaultGroups;
CanonicalGroup org = orgGroups.get(0); // just to be explicit
CanonicalGroup role = super.createRole("PSC:GN_REVIEWER"); // role name in external repo
List<CanonicalGroup> orgs = new ArrayList<>(super.defaultGroups);
orgs.add(org);

// Create a user that belongs to organization "PSC" and has role "PSC:GN_REVIEWER"
CanonicalUser u = super.setUpNewUser("prefixed", org, role);

when(canonicalAccountsRepositoryMock.findAllOrganizations()).thenReturn(orgs);
when(canonicalAccountsRepositoryMock.findAllUsers()).thenReturn(List.of(u));

service.synchronize();

// verify group link exists for the organization and user is linked to it
//support.assertGroupLinkassertUserLink(org);
UserLink link = support.assertUserLink(u);
support.assertGroup(link.getInternalUser(), org);
}


private void verify(List<CanonicalUser> expectedUsers, List<CanonicalGroup> expectedOrgs) {
assertEquals(expectedOrgs.size(), support.groupLinkRepository.findAll().size());
assertEquals(expectedUsers.size(), support.userLinkRepository.findAll().size());

for (CanonicalGroup expected : expectedOrgs) {
support.assertGroupLink(expected);
}
for (CanonicalUser expected : expectedUsers) {
support.assertUserLink(expected);
}
}
}
Loading