Skip to content

Commit 1f8ad87

Browse files
committed
Optionally allow to also remove IMS group admins which are no longer
configured Add toString, equals and hashCode methods to deserialized JSON objects This closes #798
1 parent 1a3bdb7 commit 1f8ad87

15 files changed

+468
-28
lines changed

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagement.java

+119-19
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@
2626
import java.util.Collections;
2727
import java.util.HashMap;
2828
import java.util.HashSet;
29+
import java.util.LinkedHashSet;
2930
import java.util.LinkedList;
3031
import java.util.List;
32+
import java.util.ListIterator;
3133
import java.util.Locale;
3234
import java.util.Map;
3335
import java.util.Random;
36+
import java.util.Set;
3437
import java.util.concurrent.atomic.AtomicInteger;
3538
import java.util.function.Function;
3639
import java.util.stream.Collectors;
@@ -80,6 +83,8 @@
8083
import biz.netcentric.cq.tools.actool.ims.request.AddGroupMembers;
8184
import biz.netcentric.cq.tools.actool.ims.request.AddGroupMembership;
8285
import biz.netcentric.cq.tools.actool.ims.request.CreateGroupStep;
86+
import biz.netcentric.cq.tools.actool.ims.request.RemoveGroupMembership;
87+
import biz.netcentric.cq.tools.actool.ims.request.Step;
8388
import biz.netcentric.cq.tools.actool.ims.request.UserActionCommand;
8489
import biz.netcentric.cq.tools.actool.ims.request.UserGroupActionCommand;
8590
import biz.netcentric.cq.tools.actool.ims.response.AccessToken;
@@ -231,6 +236,115 @@ private boolean requireGroupUpdate(Map<String, IMSGroup> existingImsGroups, Auth
231236
}
232237
}
233238

239+
private static final class UserMembershipChanges {
240+
private final String userName;
241+
private final Set<String> groupsToRemove;
242+
private final Set<String> groupsToAdd;
243+
244+
public UserMembershipChanges(String userName) {
245+
this.userName = userName;
246+
this.groupsToRemove = new LinkedHashSet<>();
247+
this.groupsToAdd = new LinkedHashSet<>();
248+
}
249+
250+
public boolean removeGroup(String group) {
251+
return groupsToRemove.add(group);
252+
}
253+
254+
public boolean addGroup(String group) {
255+
return groupsToAdd.add(group);
256+
}
257+
258+
public boolean addGroups(Collection<String> groups) {
259+
return groupsToAdd.addAll(groups);
260+
}
261+
}
262+
263+
/**
264+
*
265+
* @param token the api token
266+
* @param groups the groups whose administrators should be updated
267+
* @param isAddOnly whether only new group administrators should be added or also existing ones bound to managed groups but no longer configured as admins should be removed
268+
* @return the list of action commands to be executed to perform the changes
269+
* @throws IOException
270+
*/
271+
List<ActionCommand> updateGroupAdminsCommands(String token, Collection<String> groups, boolean isAddOnly) throws IOException {
272+
List<ActionCommand> actionCommands = new LinkedList<>();
273+
// optionally make users group administrators
274+
if (config.groupAdmins() != null && config.groupAdmins().length > 0) {
275+
Set<String> groupAdmins = new LinkedHashSet<>(Arrays.asList(config.groupAdmins()));
276+
Map<String, UserMembershipChanges> usersMembershipChanges = new HashMap<>();
277+
// for all admin groups collect users to remove and to add
278+
Set<String> adminGroupNames = groups.stream().map(n -> "_admin_" + n).collect(Collectors.toSet());
279+
if (!isAddOnly) {
280+
// check which membership changes are necessary per user
281+
for (String adminGroupName : adminGroupNames) {
282+
Map<String, IMSUser> usersInAdminGroup = getUsersInGroup(token, adminGroupName);
283+
// diff against configured admins and remove/add memberhip accordingly
284+
Set<String> usersToRemove = new LinkedHashSet<>(usersInAdminGroup.keySet());
285+
usersToRemove.removeAll(groupAdmins);
286+
for (String userToRemove : usersToRemove) {
287+
UserMembershipChanges changes = usersMembershipChanges.computeIfAbsent(userToRemove, UserMembershipChanges::new);
288+
changes.removeGroup(adminGroupName);
289+
}
290+
291+
Set<String> usersToAdd = new LinkedHashSet<>(groupAdmins);
292+
usersToAdd.removeAll(usersInAdminGroup.keySet());
293+
for (String userToAdd : usersToAdd) {
294+
UserMembershipChanges changes = usersMembershipChanges.computeIfAbsent(userToAdd, UserMembershipChanges::new);
295+
changes.addGroup(adminGroupName);
296+
}
297+
}
298+
} else {
299+
// just add all adminGroupNames to all admin users
300+
for (String admin : config.groupAdmins()) {
301+
UserMembershipChanges userMembershipChanges = new UserMembershipChanges(admin);
302+
userMembershipChanges.addGroups(adminGroupNames);
303+
usersMembershipChanges.put(admin, userMembershipChanges);
304+
}
305+
}
306+
307+
// iterate over all usersMembershipChanges and create user action commands per affected user
308+
for (UserMembershipChanges changes : usersMembershipChanges.values()) {
309+
int actionCommandsIndex = actionCommands.size();
310+
addMembershipSteps(changes.userName, actionCommands.listIterator(actionCommandsIndex), false, changes.groupsToRemove);
311+
addMembershipSteps(changes.userName, actionCommands.listIterator(actionCommandsIndex), true, changes.groupsToAdd);
312+
}
313+
314+
}
315+
return actionCommands;
316+
}
317+
318+
/**
319+
* Adds the necessary user memberhips steps to the next action command from the given iterator. If there is no next action command, a new one is created and added to the list.
320+
* It makes sure that per action command there are at most {@link #MAX_NUM_GROUPS_PER_ADD_STEP} groups added or removed.
321+
* @param userName used when new ActionCommand is created
322+
* @param actionCommandsIterator the ListIterator which is potentially extended with new action commands
323+
* @param isAdd {@code} true if the group membership should be added, {@code false} if it should be removed
324+
* @param groupNames the group names to add/remove for the user
325+
*/
326+
private void addMembershipSteps(String userName, ListIterator<ActionCommand> actionCommandsIterator, boolean isAdd, Collection<String> groupNames) {
327+
AtomicInteger groupCounter = new AtomicInteger();
328+
Collection<List<String>> groupNamesBatches = groupNames.stream().collect(Collectors.groupingBy
329+
(it->groupCounter.getAndIncrement() / MAX_NUM_GROUPS_PER_ADD_STEP)).values();
330+
for (List<String> groupNamesBatch : groupNamesBatches) {
331+
ActionCommand actionCommand;
332+
if (actionCommandsIterator.hasNext()) {
333+
actionCommand = actionCommandsIterator.next();
334+
} else {
335+
actionCommand = new UserActionCommand(userName);
336+
actionCommandsIterator.add(actionCommand);
337+
}
338+
final Step step;
339+
if (isAdd) {
340+
step = new AddGroupMembership(groupNamesBatch);
341+
} else {
342+
step = new RemoveGroupMembership(groupNamesBatch);
343+
}
344+
actionCommand.addStep(step);
345+
}
346+
}
347+
234348
@Override
235349
public int updateGroups(Collection<AuthorizableConfigBean> groupConfigs) throws IOException {
236350
String token = getOAuthServer2ServerToken();
@@ -259,23 +373,9 @@ public int updateGroups(Collection<AuthorizableConfigBean> groupConfigs) throws
259373
updatedGroupNames.add(groupConfig.getAuthorizableId());
260374
actionCommands.add(actionCommand);
261375
}
262-
// optionally make users group administrators
263-
if (config.groupAdmins() != null && config.groupAdmins().length > 0) {
264-
// at most 10 groups per add command
265-
AtomicInteger groupCounter = new AtomicInteger();
266-
Collection<List<String>> adminGroupNameBatches = updatedGroupNames.stream()
267-
.map(id -> "_admin_" + id) // https://adobe-apiplatform.github.io/umapi-documentation/en/api/ActionsCmds.html#addRemoveAttr
268-
.collect(Collectors.groupingBy
269-
(it->groupCounter.getAndIncrement() / MAX_NUM_GROUPS_PER_ADD_STEP)).values();
270-
for (List<String> adminGroupNames : adminGroupNameBatches) {
271-
for (String groupAdmin : config.groupAdmins()) {
272-
ActionCommand actionCommand = new UserActionCommand(groupAdmin);
273-
AddGroupMembership addGroupMembership = new AddGroupMembership(adminGroupNames);
274-
actionCommand.addStep(addGroupMembership);
275-
actionCommands.add(actionCommand);
276-
}
277-
}
278-
}
376+
377+
actionCommands.addAll(updateGroupAdminsCommands(token, updatedGroupNames, false));
378+
279379
// update in batches of 10 commands
280380
AtomicInteger counter = new AtomicInteger();
281381
final Collection<List<ActionCommand>> actionCommandsBatches = actionCommands.stream().collect(Collectors.groupingBy
@@ -370,7 +470,7 @@ public GroupResponse handleResponse(
370470
if (entity == null) {
371471
throw new ClientProtocolException("Response contains no content for request " + getRequestInfo(httpGet));
372472
}
373-
//System.out.println(EntityUtils.toString(entity));
473+
System.out.println(EntityUtils.toString(entity));
374474
GroupResponse groupResponse = objectMapper.readValue(entity.getContent(), GroupResponse.class);
375475
groupResponse.associatedRequest = httpGet;
376476
return groupResponse;
@@ -401,7 +501,7 @@ Map<String, IMSUser> getUsersInGroup(String token, String name) throws IOExcepti
401501
return users;
402502
}
403503

404-
private UsersInGroupResponse getUsersInGroup(String token, String name, int page) throws IOException {
504+
UsersInGroupResponse getUsersInGroup(String token, String name, int page) throws IOException {
405505
ObjectMapper objectMapper = new ObjectMapper();
406506
HttpGet httpGet;
407507
try {

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/request/ActionCommand.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515

1616
import java.util.Collection;
1717
import java.util.LinkedList;
18+
import java.util.Objects;
1819

1920
import com.fasterxml.jackson.annotation.JsonProperty;
2021

2122
public class ActionCommand {
2223

23-
@JsonProperty("do")
24+
@JsonProperty(value = "do", required = true)
2425
final Collection<Step> steps;
2526

2627
public ActionCommand() {
@@ -30,4 +31,26 @@ public ActionCommand() {
3031
public boolean addStep(Step step) {
3132
return steps.add(step);
3233
}
34+
35+
@Override
36+
public String toString() {
37+
return "ActionCommand [steps=" + steps + "]";
38+
}
39+
40+
@Override
41+
public int hashCode() {
42+
return Objects.hash(steps);
43+
}
44+
45+
@Override
46+
public boolean equals(Object obj) {
47+
if (this == obj)
48+
return true;
49+
if (obj == null)
50+
return false;
51+
if (getClass() != obj.getClass())
52+
return false;
53+
ActionCommand other = (ActionCommand) obj;
54+
return Objects.equals(steps, other.steps);
55+
}
3356
}

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/request/AddGroupMembers.java

+24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package biz.netcentric.cq.tools.actool.ims.request;
22

3+
import java.util.Objects;
4+
35
/*-
46
* #%L
57
* Access Control Tool Bundle
@@ -32,4 +34,26 @@ public class AddGroupMembers implements Step {
3234

3335
@JsonProperty("productConfiguration")
3436
public Set<String> productProfileIds;
37+
38+
@Override
39+
public String toString() {
40+
return "AddGroupMembers [userIds=" + userIds + ", productProfileIds=" + productProfileIds + "]";
41+
}
42+
43+
@Override
44+
public int hashCode() {
45+
return Objects.hash(productProfileIds, userIds);
46+
}
47+
48+
@Override
49+
public boolean equals(Object obj) {
50+
if (this == obj)
51+
return true;
52+
if (obj == null)
53+
return false;
54+
if (getClass() != obj.getClass())
55+
return false;
56+
AddGroupMembers other = (AddGroupMembers) obj;
57+
return Objects.equals(productProfileIds, other.productProfileIds) && Objects.equals(userIds, other.userIds);
58+
}
3559
}

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/request/AddGroupMembership.java

+28-4
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,49 @@
1414
*/
1515

1616
import java.util.Collection;
17-
import java.util.HashSet;
17+
import java.util.LinkedHashSet;
18+
import java.util.Objects;
1819
import java.util.Set;
1920

2021
import com.fasterxml.jackson.annotation.JsonInclude;
2122
import com.fasterxml.jackson.annotation.JsonInclude.Include;
2223
import com.fasterxml.jackson.annotation.JsonProperty;
2324
import com.fasterxml.jackson.annotation.JsonTypeName;
2425

25-
/** Maintains memberships of users in (admin) groups, to be used with {@link UserActionCommand}.
26+
/**
27+
* Maintains memberships of users in (admin) groups, to be used with {@link UserActionCommand}.
2628
* @see AddGroupMembers
2729
*/
2830
@JsonTypeName("add")
2931
@JsonInclude(Include.NON_EMPTY) // neither empty strings nor null values are allowed for the fields
3032
public class AddGroupMembership implements Step {
3133

3234
public AddGroupMembership(Collection<String> group) {
33-
this.group = new HashSet<>(group);
35+
this.group = new LinkedHashSet<>(group);
3436
}
3537

36-
@JsonProperty("group")
38+
@JsonProperty(value = "group", required = true)
3739
public Set<String> group;
40+
41+
@Override
42+
public String toString() {
43+
return "AddGroupMembership [group=" + group + "]";
44+
}
45+
46+
@Override
47+
public int hashCode() {
48+
return Objects.hash(group);
49+
}
50+
51+
@Override
52+
public boolean equals(Object obj) {
53+
if (this == obj)
54+
return true;
55+
if (obj == null)
56+
return false;
57+
if (getClass() != obj.getClass())
58+
return false;
59+
AddGroupMembership other = (AddGroupMembership) obj;
60+
return Objects.equals(group, other.group);
61+
}
3862
}

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/request/CreateGroupStep.java

+24
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package biz.netcentric.cq.tools.actool.ims.request;
22

3+
import java.util.Objects;
4+
35
/*-
46
* #%L
57
* Access Control Tool Bundle
@@ -30,4 +32,26 @@ public class CreateGroupStep implements Step {
3032
//String name;
3133
@JsonProperty
3234
public String description; // this may be empty
35+
36+
@Override
37+
public String toString() {
38+
return "CreateGroupStep [defaultOption=" + defaultOption + ", description=" + description + "]";
39+
}
40+
41+
@Override
42+
public int hashCode() {
43+
return Objects.hash(defaultOption, description);
44+
}
45+
46+
@Override
47+
public boolean equals(Object obj) {
48+
if (this == obj)
49+
return true;
50+
if (obj == null)
51+
return false;
52+
if (getClass() != obj.getClass())
53+
return false;
54+
CreateGroupStep other = (CreateGroupStep) obj;
55+
return Objects.equals(defaultOption, other.defaultOption) && Objects.equals(description, other.description);
56+
}
3357
}

0 commit comments

Comments
 (0)