From 801533d3278cbf1e123cec8cec0603389afec9b9 Mon Sep 17 00:00:00 2001 From: Konrad Windszus Date: Fri, 2 Aug 2024 13:19:13 +0200 Subject: [PATCH] Allow differential IMS updates It is opt-in though, because it doesn't always speed up performance. This closes #757 --- .../AuthorizableInstallerServiceImpl.java | 4 +- .../ExternalGroupManagement.java | 8 +- .../tools/actool/ims/IMSUserManagement.java | 184 ++++++++++++++++-- .../ims/response/ActionCommandResponse.java | 2 +- .../actool/ims/response/GroupResponse.java | 38 ++++ .../tools/actool/ims/response/IMSGroup.java | 72 +++++++ .../cq/tools/actool/ims/response/IMSUser.java | 87 +++++++++ .../ims/response/UsersInGroupResponse.java | 41 ++++ .../tools/actool/ims/IMSUserManagementIT.java | 74 +++++-- 9 files changed, 477 insertions(+), 33 deletions(-) create mode 100644 accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/GroupResponse.java create mode 100644 accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSGroup.java create mode 100644 accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSUser.java create mode 100644 accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/UsersInGroupResponse.java diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/AuthorizableInstallerServiceImpl.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/AuthorizableInstallerServiceImpl.java index 65e2d063..c27d4ddc 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/AuthorizableInstallerServiceImpl.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/authorizableinstaller/impl/AuthorizableInstallerServiceImpl.java @@ -145,8 +145,8 @@ private void syncWithExternalGroupManagement(Collection return; } for (ExternalGroupManagement externalGroupManagement : externalGroupManagementServices) { - externalGroupManagement.updateGroups(groupConfigBeans); - installLog.addMessage(LOG, "Synchronized " + groupConfigBeans.size() + " groups with external user management " + externalGroupManagement.getLabel()); + int numGroupsSynced = externalGroupManagement.updateGroups(groupConfigBeans); + installLog.addMessage(LOG, "Synchronized " + numGroupsSynced + " groups with external user management " + externalGroupManagement.getLabel()); } } diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/externalusermanagement/ExternalGroupManagement.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/externalusermanagement/ExternalGroupManagement.java index a026dada..3ba54265 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/externalusermanagement/ExternalGroupManagement.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/externalusermanagement/ExternalGroupManagement.java @@ -22,7 +22,13 @@ * Implementations of this service synchronize (i.e. create/update/delete) groups in an external directory (outside AEM). */ public interface ExternalGroupManagement { - void updateGroups(Collection groupConfigs) throws IOException; + /** + * Updates the groups in the external directory. + * @param groupConfigs the groups to be updated + * @return the effective number of groups updated (may be less than the number of groups in {@code groupConfigs}) if some are considered up to date + * @throws IOException + */ + int updateGroups(Collection groupConfigs) throws IOException; /** * diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagement.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagement.java index bdfbf4f7..9bb0fb13 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagement.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagement.java @@ -23,18 +23,23 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Random; -import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.http.Consts; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpMessage; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; @@ -45,6 +50,7 @@ import org.apache.http.client.ServiceUnavailableRetryStrategy; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; @@ -78,6 +84,10 @@ import biz.netcentric.cq.tools.actool.ims.request.UserGroupActionCommand; import biz.netcentric.cq.tools.actool.ims.response.AccessToken; import biz.netcentric.cq.tools.actool.ims.response.ActionCommandResponse; +import biz.netcentric.cq.tools.actool.ims.response.GroupResponse; +import biz.netcentric.cq.tools.actool.ims.response.IMSGroup; +import biz.netcentric.cq.tools.actool.ims.response.IMSUser; +import biz.netcentric.cq.tools.actool.ims.response.UsersInGroupResponse; /** * Managing Adobe IMS groups via the UMAPI. @@ -90,8 +100,8 @@ public class IMSUserManagement implements ExternalGroupManagement { @ObjectClassDefinition(name = "AC Tool Adobe IMS User Management", description = "Settings of the API for user management tasks (UMAPI) in the Adobe IMS") protected static @interface Configuration { - @AttributeDefinition(name = "UMAPI Base URL", description = "UMAPI Endpoint Base URL (the part prior the organization id)") - String umapiBaseUrl() default "https://usermanagement.adobe.io/v2/usermanagement/action/"; + @AttributeDefinition(name = "UMAPI Base URL", description = "UMAPI Endpoint Base URL (the common part of all UMAPI endpoints)") + String umapiBaseUrl() default "https://usermanagement.adobe.io/v2/usermanagement/"; @AttributeDefinition(name = "Organization ID", description = "The unique identifier for an organization. This is a string of the form A495E53@AdobeOrg where the prefix before the @ is a hexadecimal number. You can find this value as part of the URL path for the organization in the Adobe Admin Console or in the Adobe Developer Console for your User Management integration.") String organizationId(); @AttributeDefinition(name = "Test Only", description = "If true, parameter syntactic and (limited) semantic checking is done, but the specified operations are not performed, so no user/group accounts or group memberships are created, changed, or deleted.") @@ -112,6 +122,8 @@ public class IMSUserManagement implements ExternalGroupManagement { String[] productProfiles() default {}; @AttributeDefinition(name = "Group Administrators", description = "The given users are automatically added to each synchronized IMS group as administrator. The given user ids must already exist!") String[] groupAdmins() default {}; + @AttributeDefinition(name = "Differential Updates", description = "If true, only groups that have changed are updated. This is only a heuristics and currently leads to only adding new groups but never updating existing group (with same names). Also in some cases the additional request to get all groups may be more expensive than just updating all groups.") + boolean isDifferentialUpdates() default false; } public static final Logger LOG = LoggerFactory.getLogger(IMSUserManagement.class); @@ -188,12 +200,16 @@ public void deactivate() throws IOException { } private URI getUserManagementActionUrl() throws URISyntaxException { - URI uri = new URI(config.umapiBaseUrl() + config.organizationId()); - if (config.isTestOnly()) { - uri = new URI(uri.getScheme(), uri.getAuthority(), - uri.getPath(), "testOnly=true", uri.getFragment()); - } - return uri; + return new URI(config.umapiBaseUrl()).resolve(new URI(null, null, "action/"+ config.organizationId(), config.isTestOnly() ? "testOnly=true" : null, null)); + } + + private URI getUserManagementGroupsUrl(int page) throws URISyntaxException { + return new URI(config.umapiBaseUrl()).resolve(new URI(null, null, "groups/"+ config.organizationId() + "/" + page, null)); + } + + private URI getUserManagementUsersInGroupUrl(int page, String groupName) throws URISyntaxException { + // group names may have spaces and require percent encoding as defined by RFC 3986 + return new URI(config.umapiBaseUrl()).resolve(new URI(null, null, "users/"+ config.organizationId() + "/" + page+ "/" + groupName, null)); } @Override @@ -201,10 +217,32 @@ public String getLabel() { return "Adobe IMS"; } + private boolean requireGroupUpdate(Map existingImsGroups, AuthorizableConfigBean groupConfig) { + // since group names are case insensitive always compare in lower case + IMSGroup existingImsGroup = existingImsGroups.get(groupConfig.getAuthorizableId().toLowerCase(Locale.ROOT)); + if (existingImsGroup == null) { + LOG.debug("Group {} does not exist yet", groupConfig.getAuthorizableId()); + return true; + } else { + // this is just a heuristics as the group description is not returned (https://adminconsole.adobe.com/FA907D44536A3C2B0A490D4D@AdobeOrg/support/support-cases/E-001297310) + return false; + } + } + @Override - public void updateGroups(Collection groupConfigs) throws IOException { + public int updateGroups(Collection groupConfigs) throws IOException { + String token = getOAuthServer2ServerToken(); + Map existingImsGroups = Collections.emptyMap(); + if (config.isDifferentialUpdates()) { + existingImsGroups = getGroups(token); + } List actionCommands = new LinkedList<>(); + List updatedGroupNames = new LinkedList<>(); for (AuthorizableConfigBean groupConfig : groupConfigs) { + if (config.isDifferentialUpdates() && !requireGroupUpdate(existingImsGroups, groupConfig)) { + LOG.info("Skip updating IMS group {}, as considered up to date "); + continue; + } ActionCommand actionCommand = new UserGroupActionCommand(groupConfig.getAuthorizableId()); CreateGroupStep createGroupStep = new CreateGroupStep(); createGroupStep.description = groupConfig.getDescription(); @@ -215,14 +253,14 @@ public void updateGroups(Collection groupConfigs) throws addMembers.productProfileIds = new HashSet<>(Arrays.asList(config.productProfiles())); actionCommand.addStep(addMembers); } + updatedGroupNames.add(groupConfig.getAuthorizableId()); actionCommands.add(actionCommand); } // optionally make users group administrators if (config.groupAdmins() != null && config.groupAdmins().length > 0) { // at most 10 groups per add command AtomicInteger groupCounter = new AtomicInteger(); - Collection> adminGroupNameBatches = groupConfigs.stream() - .map(AuthorizableConfigBean::getAuthorizableId) + Collection> adminGroupNameBatches = updatedGroupNames.stream() .map(id -> "_admin_" + id) // https://adobe-apiplatform.github.io/umapi-documentation/en/api/ActionsCmds.html#addRemoveAttr .collect(Collectors.groupingBy (it->groupCounter.getAndIncrement() / MAX_NUM_GROUPS_PER_ADD_STEP)).values(); @@ -240,7 +278,7 @@ public void updateGroups(Collection groupConfigs) throws final Collection> actionCommandsBatches = actionCommands.stream().collect(Collectors.groupingBy (it->counter.getAndIncrement() / MAX_NUM_COMMANDS_PER_REQUEST)) .values(); - String token = getOAuthServer2ServerToken(); + for (List actionCommandBatch : actionCommandsBatches) { ActionCommandResponse response = sendActionCommand(token, actionCommandBatch); if (!response.errors.isEmpty()) { @@ -251,6 +289,7 @@ public void updateGroups(Collection groupConfigs) throws response.warnings.stream().forEach(w -> LOG.warn("Warning updating a group: {}", w)); } } + return updatedGroupNames.size(); } static String getRequestInfo(HttpRequest request) throws IOException { @@ -268,6 +307,118 @@ static String getRequestInfo(HttpRequest request) throws IOException { return requestInfo.toString(); } + private void setHttpAuthenticationHeaders(HttpMessage httpMessage, String token) { + httpMessage.setHeader("Authorization", "Bearer " + token); + httpMessage.setHeader("X-Api-Key", config.clientId()); + + } + + /** + * Retrieves all groups and product profiles with the API endpoint described at Get User Groups and Product Profiles. + *

+ * Maximum 5 requests per minute per a client. + * @param token the access token + * @throws IOException + * @return a map with group names (lower-case) as keys and {@link IMSGroup}s as values + */ + Map getGroups(String token) throws IOException { + int page = 0; + boolean isLastPage = false; + Map groups = new HashMap<>(); + while (!isLastPage) { + GroupResponse response = getGroups(token, page++); + isLastPage = response.isLastPage; + groups.putAll(response.groups.stream().collect(Collectors.toMap(g -> g.getGroupName().toLowerCase(Locale.ROOT), Function.identity()))); + } + return groups; + } + + private GroupResponse getGroups(String token, int page) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + HttpGet httpGet; + try { + httpGet = new HttpGet(getUserManagementGroupsUrl(page)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Could not create valid URI from configuration", e); + } + setHttpAuthenticationHeaders(httpGet, token); + ResponseHandler rh = new ResponseHandler() { + @Override + public GroupResponse handleResponse( + final HttpResponse response) throws IOException { + StatusLine statusLine = response.getStatusLine(); + HttpEntity entity = response.getEntity(); + if (statusLine.getStatusCode() >= 300) { + throw new HttpResponseException( + statusLine.getStatusCode(), + statusLine.getReasonPhrase() + ", body:" + EntityUtils.toString(entity) + ", for request " + getRequestInfo(httpGet)); + } + if (entity == null) { + throw new ClientProtocolException("Response contains no content for request " + getRequestInfo(httpGet)); + } + //System.out.println(EntityUtils.toString(entity)); + GroupResponse groupResponse = objectMapper.readValue(entity.getContent(), GroupResponse.class); + groupResponse.associatedRequest = httpGet; + return groupResponse; + } + }; + LOG.debug("Calling UMAPI via {}", httpGet); + return client.execute(httpGet, rh); + } + + /** + * Retrieves all members of a group with the API endpoint described at Get Users in a User Group or Product Profile. + *

+ * Maximum 25 requests per minute per a client. + * @param token the access token + * @param name the group name + * @throws IOException + * @return a map with group names as keys and {@link IMSGroup}s as values + */ + Map getUsersInGroup(String token, String name) throws IOException { + int page = 0; + boolean isLastPage = false; + Map users = new HashMap<>(); + while (!isLastPage) { + UsersInGroupResponse response = getUsersInGroup(token, name, page++); + isLastPage = response.isLastPage; + users.putAll(response.users.stream().collect(Collectors.toMap(IMSUser::getUsername, Function.identity()))); + } + return users; + } + + private UsersInGroupResponse getUsersInGroup(String token, String name, int page) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + HttpGet httpGet; + try { + httpGet = new HttpGet(getUserManagementUsersInGroupUrl(page, name)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Could not create valid URI from configuration", e); + } + setHttpAuthenticationHeaders(httpGet, token); + ResponseHandler rh = new ResponseHandler() { + @Override + public UsersInGroupResponse handleResponse( + final HttpResponse response) throws IOException { + StatusLine statusLine = response.getStatusLine(); + HttpEntity entity = response.getEntity(); + if (statusLine.getStatusCode() >= 300) { + throw new HttpResponseException( + statusLine.getStatusCode(), + statusLine.getReasonPhrase() + ", body:" + EntityUtils.toString(entity) + ", for request " + getRequestInfo(httpGet)); + } + if (entity == null) { + throw new ClientProtocolException("Response contains no content for request " + getRequestInfo(httpGet)); + } + UsersInGroupResponse groupResponse = objectMapper.readValue(entity.getContent(), UsersInGroupResponse.class); + groupResponse.associatedRequest = httpGet; + return groupResponse; + } + }; + LOG.debug("Calling UMAPI via {}", httpGet); + return client.execute(httpGet, rh); + } + private ActionCommandResponse sendActionCommand(String token, Collection actions) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); HttpPost httpPost; @@ -278,8 +429,7 @@ private ActionCommandResponse sendActionCommand(String token, Collection rh = new ResponseHandler() { @Override public ActionCommandResponse handleResponse( @@ -292,7 +442,7 @@ public ActionCommandResponse handleResponse( statusLine.getReasonPhrase() + ", body:" + EntityUtils.toString(entity) + ", for request " + getRequestInfo(httpPost)); } if (entity == null) { - throw new ClientProtocolException("Response contains no content for request" + getRequestInfo(httpPost)); + throw new ClientProtocolException("Response contains no content for request " + getRequestInfo(httpPost)); } ActionCommandResponse actionCommandResponse = objectMapper.readValue(entity.getContent(), ActionCommandResponse.class); actionCommandResponse.associatedRequest = httpPost; @@ -310,7 +460,7 @@ public ActionCommandResponse handleResponse( * @throws IOException * @see OAuth Server to Server Authentication */ - private String getOAuthServer2ServerToken() throws IOException { + String getOAuthServer2ServerToken() throws IOException { HttpPost httpPost = new HttpPost(config.imsTokenEndpointUrl()); List params = new ArrayList<>(); params.add(new BasicNameValuePair("client_id", config.clientId())); diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/ActionCommandResponse.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/ActionCommandResponse.java index 119aba88..bffa474f 100644 --- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/ActionCommandResponse.java +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/ActionCommandResponse.java @@ -22,7 +22,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -/** General response format for UMAPI requests */ +/** General response format for UMAPI action requests */ @JsonIgnoreProperties(ignoreUnknown = true) public class ActionCommandResponse { diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/GroupResponse.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/GroupResponse.java new file mode 100644 index 00000000..72fcc7bf --- /dev/null +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/GroupResponse.java @@ -0,0 +1,38 @@ +package biz.netcentric.cq.tools.actool.ims.response; + +/*- + * #%L + * Access Control Tool Bundle + * %% + * Copyright (C) 2015 - 2024 Cognizant Netcentric + * %% + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * #L% + */ + +import java.util.List; + +import org.apache.http.client.methods.HttpRequestBase; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class GroupResponse { + + @JsonProperty("lastPage") + public boolean isLastPage; + + @JsonProperty("result") + public String result; + + @JsonProperty("groups") + public List groups; + + @JsonIgnore + public HttpRequestBase associatedRequest; +} diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSGroup.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSGroup.java new file mode 100644 index 00000000..1e32b96d --- /dev/null +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSGroup.java @@ -0,0 +1,72 @@ +package biz.netcentric.cq.tools.actool.ims.response; + +/*- + * #%L + * Access Control Tool Bundle + * %% + * Copyright (C) 2015 - 2024 Cognizant Netcentric + * %% + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * #L% + */ + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** Represents either a user group or product profile in IMS. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class IMSGroup { + + @JsonProperty("type") + public String type; + + @JsonProperty("memberCount") + public int memberCount; + + @JsonProperty("adminGroupName") + public String adminGroupName; + + @JsonProperty("groupName") + public String groupName; + + @JsonProperty("groupId") + public long groupId; + + @JsonProperty("userGroupName") + public String userGroupName; + + @Override + public String toString() { + return "IMSGroup [type=" + type + ", memberCount=" + memberCount + ", adminGroupName=" + adminGroupName + ", groupName=" + groupName + + ", groupId=" + groupId + ", userGroupName=" + userGroupName + "]"; + } + + public String getType() { + return type; + } + + public int getMemberCount() { + return memberCount; + } + + public String getAdminGroupName() { + return adminGroupName; + } + + public String getGroupName() { + return groupName; + } + + public long getGroupId() { + return groupId; + } + + public String getUserGroupName() { + return userGroupName; + } + + +} diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSUser.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSUser.java new file mode 100644 index 00000000..66506029 --- /dev/null +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/IMSUser.java @@ -0,0 +1,87 @@ +package biz.netcentric.cq.tools.actool.ims.response; + +/*- + * #%L + * Access Control Tool Bundle + * %% + * Copyright (C) 2015 - 2024 Cognizant Netcentric + * %% + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * #L% + */ + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class IMSUser { + + @JsonProperty("type") + public String type; + + @JsonProperty("email") + public String email; + + @JsonProperty("status") + public String status; + + @JsonProperty("groups") + public List groups; + + @JsonProperty("domain") + public String domain; + + @JsonProperty("country") + public String country; + + @JsonProperty("tags") + public List tags; + + @JsonProperty("username") + public String username; + + @Override + public String toString() { + return "IMSUser [type=" + type + ", email=" + email + ", status=" + status + ", groups=" + groups + ", domain=" + domain + + ", country=" + country + ", tags=" + tags + ", username=" + username + "]"; + } + + public String getType() { + return type; + } + + public String getEmail() { + return email; + } + + public String getStatus() { + return status; + } + + public List getGroups() { + return groups; + } + + public String getDomain() { + return domain; + } + + public String getCountry() { + return country; + } + + public List getTags() { + return tags; + } + + public String getUsername() { + return username; + } + + +} diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/UsersInGroupResponse.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/UsersInGroupResponse.java new file mode 100644 index 00000000..572e47c2 --- /dev/null +++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ims/response/UsersInGroupResponse.java @@ -0,0 +1,41 @@ +package biz.netcentric.cq.tools.actool.ims.response; + +/*- + * #%L + * Access Control Tool Bundle + * %% + * Copyright (C) 2015 - 2024 Cognizant Netcentric + * %% + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * #L% + */ + +import java.util.List; + +import org.apache.http.client.methods.HttpRequestBase; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class UsersInGroupResponse { + + @JsonProperty("lastPage") + public boolean isLastPage; + + @JsonProperty("result") + public String result; + + @JsonProperty("groupName") + public String groupName; + + @JsonProperty("users") + public List users; + + @JsonIgnore + public HttpRequestBase associatedRequest; +} diff --git a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagementIT.java b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagementIT.java index 7de7d1ec..89dfa65d 100644 --- a/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagementIT.java +++ b/accesscontroltool-bundle/src/test/java/biz/netcentric/cq/tools/actool/ims/IMSUserManagementIT.java @@ -33,6 +33,8 @@ import biz.netcentric.cq.tools.actool.configmodel.AuthorizableConfigBean; import biz.netcentric.cq.tools.actool.ims.IMSUserManagement.Configuration; +import biz.netcentric.cq.tools.actool.ims.response.IMSGroup; +import biz.netcentric.cq.tools.actool.ims.response.IMSUser; /** * Example Adobe Developer Console Project: https://developer.adobe.com/console/projects/25605/4566206088345177434/overview (Organization: Netcentric). @@ -56,10 +58,11 @@ void setUp() { properties.put("clientId", getMandatoryEnvironmentVariable("ACTOOL_IMS_IT_CLIENTID")); properties.put("clientSecret", getMandatoryEnvironmentVariable("ACTOOL_IMS_IT_CLIENTSECRET")); properties.put("isTestOnly", Boolean.TRUE); + properties.put("socketTimeout", "60000"); } @Test - void testSimpleGroup() throws IOException { + void testAddSimpleGroup() throws IOException { Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { @Override @@ -71,22 +74,37 @@ public HttpClientBuilder newBuilder() { AuthorizableConfigBean group = new AuthorizableConfigBean(); group.setAuthorizableId("testGroup"); group.setDescription("my description"); - imsUserManagement.updateGroups(Collections.singleton(group)); - + assertEquals(1, imsUserManagement.updateGroups(Collections.singleton(group))); + // test without description AuthorizableConfigBean group2 = new AuthorizableConfigBean(); group2.setAuthorizableId("testGroup"); - imsUserManagement.updateGroups(Collections.singleton(group2)); - + assertEquals(1, imsUserManagement.updateGroups(Collections.singleton(group2))); + // test with empty description AuthorizableConfigBean group3 = new AuthorizableConfigBean(); group3.setAuthorizableId("testGroup"); group3.setDescription(""); - imsUserManagement.updateGroups(Collections.singleton(group3)); + assertEquals(1, imsUserManagement.updateGroups(Collections.singleton(group3))); } @Test - void testGroupWithProductProfileMembership() throws IOException { + void testAddAlreadyExistingGroupInDifferentialUpdatesMode() throws IOException { + properties.put("isDifferentialUpdates", Boolean.TRUE); + Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); + IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { + @Override + public HttpClientBuilder newBuilder() { + return HttpClientBuilder.create(); + } + }); + AuthorizableConfigBean group = new AuthorizableConfigBean(); + group.setAuthorizableId("testGroup"); // this group is already there + assertEquals(0, imsUserManagement.updateGroups(Collections.singleton(group))); + } + + @Test + void testAddGroupWithProductProfileMembership() throws IOException { properties.put("productProfiles", getMandatoryEnvironmentVariable("ACTOOL_IMS_IT_PRODUCTPROFILE")); Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { @@ -102,7 +120,7 @@ public HttpClientBuilder newBuilder() { } @Test - void testGroupWithInvalidProductProfileMembership() throws IOException { + void testAddGroupWithInvalidProductProfileMembership() throws IOException { properties.put("productProfiles", "invalid"); Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { @@ -119,7 +137,7 @@ public HttpClientBuilder newBuilder() { } @Test - void testGroupWithAdmin() throws IOException { + void testAddGroupWithAdmin() throws IOException { properties.put("groupAdmins", getMandatoryEnvironmentVariable("ACTOOL_IMS_IT_USERID")); Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { @@ -131,11 +149,11 @@ public HttpClientBuilder newBuilder() { AuthorizableConfigBean group = new AuthorizableConfigBean(); group.setAuthorizableId("testGroup"); group.setDescription("my description"); - imsUserManagement.updateGroups(Collections.singleton(group)); + assertEquals(1, imsUserManagement.updateGroups(Collections.singleton(group))); } @Test - void test25GroupsWithAdmin() throws IOException { + void testAdd25GroupsWithAdmin() throws IOException { properties.put("groupAdmins", getMandatoryEnvironmentVariable("ACTOOL_IMS_IT_USERID")); Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { @@ -151,7 +169,39 @@ public HttpClientBuilder newBuilder() { group.setDescription("my description" + n); groups.add(group); } - imsUserManagement.updateGroups(groups); + assertEquals(25, imsUserManagement.updateGroups(groups)); + } + + @Test + void testGetGroups() throws IOException { + Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); + IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { + @Override + public HttpClientBuilder newBuilder() { + return HttpClientBuilder.create(); + } + }); + String token = imsUserManagement.getOAuthServer2ServerToken(); + Map groups = imsUserManagement.getGroups(token); + for (Map.Entry imsGroup : groups.entrySet()) { + System.out.println(imsGroup); + } + } + + @Test + void testGetUserInGroup() throws IOException { + Configuration config = Converters.standardConverter().convert(properties).to(Configuration.class); + IMSUserManagement imsUserManagement = new IMSUserManagement(config, new HttpClientBuilderFactory() { + @Override + public HttpClientBuilder newBuilder() { + return HttpClientBuilder.create(); + } + }); + String token = imsUserManagement.getOAuthServer2ServerToken(); + Map users = imsUserManagement.getUsersInGroup(token, "AEM Users-3e6e8bd0a05f39bc82d788bb27ac83b4"); + for (Map.Entry imsUser : users.entrySet()) { + System.out.println(imsUser); + } } private static String getMandatoryEnvironmentVariable(String name) {