-
-
Notifications
You must be signed in to change notification settings - Fork 21
New Group Sync for GN #350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
eb73d6f
feat: create a new sync type
f-necas 38191b9
feat add tests
f-necas 4c35b80
feat: deprecate roles based sync and update rolePerOrg one
f-necas 6fadd27
Apply suggestions from code review
f-necas 2ea88cc
Apply suggestions from code review
f-necas 304ea84
refactor: optimize userRoles method by reusing compiled pattern
f-necas 9f35f72
fix: update georchestra version to 25.0.3 in pom.xml
f-necas 2036d03
docs: add class-level documentation for RolePerOrgBasedGroupSynchronizer
f-necas File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
...n/java/org/geonetwork/security/external/integration/RolePerOrgBasedGroupSynchronizer.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| /* | ||
| * 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; | ||
|
|
||
| /** | ||
| * Synchronizes geOrchestra groups with external groups based on the user's organization and roles. | ||
| * For each organization, a corresponding geOrchestra group is created (if it doesn't exist) and the user is added to it if they belong to that organization. | ||
| * Additionally, if the user's roles contain an organization prefix (e.g., "ORG:ROLE"), the user is added to the group corresponding to that organization (e.g., "ORG"). | ||
| */ | ||
| public class RolePerOrgBasedGroupSynchronizer extends AbstractGroupSynchronizer { | ||
|
f-necas marked this conversation as resolved.
|
||
|
|
||
| public static final Logger log = LoggerFactory.getLogger(RolePerOrgBasedGroupSynchronizer.class.getPackage().getName()); | ||
|
|
||
| private static final String separator = ":"; | ||
|
|
||
| @Autowired | ||
|
f-necas marked this conversation as resolved.
|
||
| 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(); | ||
| Stream<String> groupsName = userRoles(user) | ||
| .flatMap(r -> { | ||
| if (r.contains(separator)) { | ||
| return Stream.of(r.split(separator)[0]); | ||
| } | ||
| return StringUtils.hasLength(orgName) ? Stream.of(orgName) : Stream.empty(); | ||
| }) | ||
| .distinct(); | ||
| Stream<CanonicalGroup> roleGroups = groupsName.map(role -> this.externalGroupLinks.findByName(role)// | ||
| .map(GroupLink::getCanonical) | ||
|
f-necas marked this conversation as resolved.
|
||
| .or(() -> canonicalAccounts.findOrganizationByName(role)) | ||
| .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) { | ||
| Pattern p = Pattern.compile(".+" + separator + ".+"); | ||
| return user.getRoles().stream() | ||
| .filter(r -> p.matcher(r).matches() || config.getProfiles().getRolemappings().containsKey(r)); | ||
| } | ||
|
f-necas marked this conversation as resolved.
|
||
|
|
||
| private Privilege resolvePrivilegeFor(CanonicalUser user, Group group) { | ||
| String groupPrefix = group.getName() + separator; | ||
| List<String> rolesForGroup = userRoles(user) | ||
| .filter(r -> { | ||
| if (r.contains(separator)) { | ||
| return r.startsWith(groupPrefix); | ||
| } else { | ||
| return group.getName().equals(user.getOrganization()); | ||
| } | ||
| }) //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) { | ||
| // Used to resolve the user's global/default profile: returns relevant role names | ||
| // with any organization prefix stripped (for example, PSC:GN_REVIEWER -> GN_REVIEWER). | ||
| return userRoles(user).map(this::getRootRole).collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private String getRootRole(String role) { | ||
| if (role.contains(separator)) { | ||
| return role.split(separator)[1]; | ||
|
f-necas marked this conversation as resolved.
|
||
| } | ||
| return role; | ||
| } | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
...t/java/org/geonetwork/security/external/integration/RolePerOrgBasedSynchronizationIT.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // 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); | ||
|
|
||
| // 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); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.