Skip to content

Commit 36160ae

Browse files
authored
Fix issues related to group types. (geonetwork#9108)
* Extract "get or create group" logic to new BaseUserUtils class * Update oidc to handle system groups in role names * Add / fix tests * Show system privileges in the users ui * Change onlyIncludeWorkspaces flag to an optional list of group types * Move default group type to the domain * Fix ShibbolethUserUtilsTest * Whitespace * Resolve comments
1 parent ca40bb7 commit 36160ae

File tree

19 files changed

+578
-234
lines changed

19 files changed

+578
-234
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright (C) 2025 Food and Agriculture Organization of the
3+
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
4+
* and United Nations Environment Programme (UNEP)
5+
*
6+
* This program is free software; you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation; either version 2 of the License, or (at
9+
* your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful, but
12+
* WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
19+
*
20+
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
21+
* Rome - Italy. email: [email protected]
22+
*/
23+
package org.fao.geonet.kernel.security;
24+
25+
import org.fao.geonet.domain.Group;
26+
import org.fao.geonet.domain.GroupType;
27+
import org.fao.geonet.domain.Language;
28+
import org.fao.geonet.repository.GroupRepository;
29+
import org.fao.geonet.repository.LanguageRepository;
30+
import org.springframework.beans.factory.annotation.Autowired;
31+
import org.springframework.stereotype.Service;
32+
33+
/**
34+
* Utility class for managing user groups in the GeoNetwork security system.
35+
* <p>
36+
* This service provides methods to retrieve or create groups by name, ensuring that groups
37+
* exist before assigning them to users. It is used or extended by various authentication providers
38+
* (JWT Headers, Keycloak, OIDC) to manage user-group associations.
39+
* </p>
40+
* <p>
41+
* When creating a new group, this utility automatically populates label translations for all
42+
* available languages in the system.
43+
* </p>
44+
*/
45+
@Service
46+
public class BaseUserUtils {
47+
48+
@Autowired
49+
LanguageRepository langRepository;
50+
51+
@Autowired
52+
GroupRepository groupRepository;
53+
54+
/**
55+
* Get or create a group by name. Defaults to Workspace type.
56+
* <p>
57+
* This method retrieves an existing group from the database by name. If the group does not
58+
* exist, a new group is created with the specified name and a default type of {@link GroupType#Workspace}.
59+
* Label translations are automatically populated for all available languages using the group name.
60+
* </p>
61+
*
62+
* @param groupName the name of the group.
63+
* @return the group (either existing or newly created).
64+
*/
65+
public Group getOrCreateGroup(String groupName) {
66+
return getOrCreateGroup(groupName, null);
67+
}
68+
69+
/**
70+
* Get or create a group by name.
71+
* <p>
72+
* This method retrieves an existing group from the database by name. If the group does not
73+
* exist, a new group is created with the specified name and type. Label translations are
74+
* automatically populated for all available languages using the group name.
75+
* </p>
76+
* <p>
77+
* This method is commonly used by authentication providers to ensure that groups referenced
78+
* in user tokens or authentication responses exist in the database before assigning users
79+
* to those groups.
80+
* </p>
81+
*
82+
* @param groupName the name of the group.
83+
* @param newGroupType the type of the group to be used if creating a new group. May be null to use the default type.
84+
* @return the group (either existing or newly created).
85+
*/
86+
public Group getOrCreateGroup(String groupName, GroupType newGroupType) {
87+
Group group = groupRepository.findByName(groupName);
88+
89+
if (group != null && group.getType() != null && !group.getType().equals(newGroupType)) {
90+
// Log a warning if the existing group's type differs from the requested type
91+
System.out.println("Warning: Group '" + groupName + "' exists with type '" + group.getType() +
92+
"', but requested type is '" + newGroupType + "'. Using existing group type.");
93+
} else if (group == null) {
94+
group = new Group();
95+
group.setName(groupName);
96+
if (newGroupType != null) {
97+
group.setType(newGroupType);
98+
}
99+
100+
// Populate languages for the group
101+
for (Language l : langRepository.findAll()) {
102+
group.getLabelTranslations().put(l.getId(), group.getName());
103+
}
104+
105+
groupRepository.save(group);
106+
}
107+
108+
return group;
109+
}
110+
111+
}

core/src/main/java/org/fao/geonet/kernel/security/jwtheaders/JwtHeadersUserUtil.java

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@
2525

2626
import org.fao.geonet.constants.Geonet;
2727
import org.fao.geonet.domain.Group;
28-
import org.fao.geonet.domain.Language;
2928
import org.fao.geonet.domain.Profile;
3029
import org.fao.geonet.domain.User;
3130
import org.fao.geonet.domain.UserGroup;
31+
import org.fao.geonet.kernel.security.BaseUserUtils;
3232
import org.fao.geonet.kernel.security.GeonetworkAuthenticationProvider;
33-
import org.fao.geonet.repository.GroupRepository;
34-
import org.fao.geonet.repository.LanguageRepository;
3533
import org.fao.geonet.repository.UserGroupRepository;
3634
import org.fao.geonet.repository.UserRepository;
3735
import org.fao.geonet.utils.Log;
@@ -46,23 +44,17 @@
4644
/**
4745
* This class handles GeoNetwork related User (and Group/UserGroup) activities.
4846
*/
49-
public class JwtHeadersUserUtil {
47+
public class JwtHeadersUserUtil extends BaseUserUtils {
5048

5149
@Autowired
5250
UserRepository userRepository;
5351

54-
@Autowired
55-
GroupRepository groupRepository;
56-
5752
@Autowired
5853
UserGroupRepository userGroupRepository;
5954

6055
@Autowired
6156
GeonetworkAuthenticationProvider authProvider;
6257

63-
@Autowired
64-
LanguageRepository languageRepository;
65-
6658
/**
6759
* Gets a user.
6860
* 1. if the user currently existing in the GN DB:
@@ -198,19 +190,7 @@ public void updateGroups(Map<Profile, List<String>> profileGroups, User user) {
198190
List<String> groups = profileGroups.get(p);
199191
for (String rgGroup : groups) {
200192

201-
Group group = groupRepository.findByName(rgGroup);
202-
203-
if (group == null) {
204-
group = new Group();
205-
group.setName(rgGroup);
206-
207-
// Populate languages for the group
208-
for (Language l : languageRepository.findAll()) {
209-
group.getLabelTranslations().put(l.getId(), group.getName());
210-
}
211-
212-
groupRepository.save(group);
213-
}
193+
Group group = getOrCreateGroup(rgGroup);
214194

215195
UserGroup usergroup = new UserGroup();
216196
usergroup.setGroup(group);

core/src/main/java/org/fao/geonet/kernel/security/keycloak/KeycloakUserUtils.java

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,10 @@
2828

2929
import org.fao.geonet.constants.Geonet;
3030
import org.fao.geonet.domain.*;
31+
import org.fao.geonet.kernel.security.BaseUserUtils;
3132
import org.fao.geonet.kernel.security.GeonetworkAuthenticationProvider;
32-
import org.fao.geonet.repository.GroupRepository;
33-
import org.fao.geonet.repository.LanguageRepository;
3433
import org.fao.geonet.repository.UserGroupRepository;
3534
import org.fao.geonet.repository.UserRepository;
36-
import org.fao.geonet.repository.specification.UserGroupSpecs;
3735
import org.fao.geonet.utils.Log;
3836
import org.keycloak.adapters.AdapterDeploymentContext;
3937
import org.keycloak.representations.AccessToken;
@@ -50,17 +48,11 @@
5048
import com.google.common.cache.Cache;
5149
import com.google.common.cache.CacheBuilder;
5250

53-
public class KeycloakUserUtils {
51+
public class KeycloakUserUtils extends BaseUserUtils {
5452

5553
@Autowired
5654
private UserRepository userRepository;
5755

58-
@Autowired
59-
private GroupRepository groupRepository;
60-
61-
@Autowired
62-
private LanguageRepository langRepository;
63-
6456
@Autowired
6557
private UserGroupRepository userGroupRepository;
6658

@@ -251,26 +243,14 @@ private Map<Profile, List<String>> getProfileGroups(AccessToken accessToken) {
251243
* @param user to apply the changes to.
252244
*/
253245
private void updateGroups(Map<Profile, List<String>> profileGroups, User user) {
254-
Set<UserGroup> userGroups = new HashSet<>();
246+
Set<UserGroup> userGroups = new HashSet<>();
255247

256248
// Now we add the groups
257249
for (Profile p : profileGroups.keySet()) {
258250
List<String> groups = profileGroups.get(p);
259251
for (String rgGroup : groups) {
260252

261-
Group group = groupRepository.findByName(rgGroup);
262-
263-
if (group == null) {
264-
group = new Group();
265-
group.setName(rgGroup);
266-
267-
// Populate languages for the group
268-
for (Language l : langRepository.findAll()) {
269-
group.getLabelTranslations().put(l.getId(), group.getName());
270-
}
271-
272-
groupRepository.save(group);
273-
}
253+
Group group = getOrCreateGroup(rgGroup);
274254

275255
UserGroup usergroup = new UserGroup();
276256
usergroup.setGroup(group);

0 commit comments

Comments
 (0)