diff --git a/README.md b/README.md index 53d0b11..6bbe0c0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # midPoint-Grouper_connector This is a connector that can read groups from a Grouper instance using REST calls. -Currently it supports these searches only: +Currently, it supports these searches only: - fetching all groups, - fetching a group by name, - fetching a group by UUID. @@ -12,4 +12,26 @@ Besides `search` operation the following ones are supported: - `schema` - `test` -This connector was tested with Grouper 2.4. \ No newline at end of file +This connector was tested with Grouper 2.5. + +//TODO: Document baseStem, sourceId, include/exclude Group, and Group Attribute Map params and how they interact based on Grouper WS + + +It's strongly recommended to add timeouts to your midPoint resource! + +```xml + + 180000 + 180000 + 180000 + 180000 + 60000 + 180000 + 180000 + 60000 + 180000 + 180000 + 180000 + 60000 + +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 06e38be..a82bed4 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ connector-parent com.evolveum.polygon - 1.4.2.14 + 1.5.0.0 @@ -35,7 +35,9 @@ com.evolveum.polygon.connector.grouper.rest GrouperRestConnector - 1.8 + 1.11 + 1.11 + UTF-8 @@ -65,6 +67,10 @@ org.apache.maven.plugins maven-compiler-plugin + + 11 + 11 + org.apache.maven.plugins @@ -75,6 +81,18 @@ maven-surefire-plugin 2.19.1 + + org.owasp + dependency-check-maven + 6.1.5 + + + + check + + + + @@ -83,22 +101,22 @@ connector-rest com.evolveum.polygon - 1.4.2.14-SNAPSHOT + 1.5.0.0 org.apache.httpcomponents httpclient - 4.5.1 + 4.5.13 org.json json - 20160810 + 20210307 org.testng testng - 6.8 + 7.4.0 test diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java index cbd144a..5a4d150 100644 --- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java @@ -17,33 +17,99 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.utils.URIBuilder; +import org.identityconnectors.common.StringUtil; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.AttributeFilter; +import org.identityconnectors.framework.common.objects.filter.ContainsFilter; import org.identityconnectors.framework.common.objects.filter.EqualsFilter; import org.identityconnectors.framework.common.objects.filter.Filter; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; - import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + /** * Contains logic for handling operations on Group object class. */ public class GroupProcessor extends Processor { + private static final String J_WS_REST_GET_MEMBERS_REQUEST = "WsRestGetMembersRequest"; + private static final String J_WS_REST_FIND_GROUPS_REQUEST = "WsRestFindGroupsRequest"; + private static final String J_WS_REST_FIND_STEMS_REQUEST = "WsRestFindStemsRequest"; + private static final String J_WS_REST_ATTRIBUTE_ASSIGNMENT_LITE_REQUEST = "WsRestGetAttributeAssignmentsLiteRequest"; + + private static final String J_WS_QUERY_FILTER = "wsQueryFilter"; + private static final String J_WS_STEM_QUERY_FILTER = "wsStemQueryFilter"; + private static final String J_STEM_QUERY_FILTER_TYPE = "stemQueryFilterType"; + private static final String J_INCLUDE_SUBJECT_DETAIL = "includeSubjectDetail"; + private static final String J_QUERY_FILTER_TYPE = "queryFilterType"; + private static final String J_STEM_NAME = "stemName"; + private static final String J_STEM_NAME_SCOPE = "stemNameScope"; + private static final String J_GROUP_NAME = "groupName"; + + private static final String J_WS_FIND_GROUPS_RESULTS = "WsFindGroupsResults"; + private static final String J_WS_FIND_STEMS_RESULTS = "WsFindStemsResults"; + private static final String J_WS_GET_MEMBERS_RESULTS = "WsGetMembersResults"; + private static final String J_WS_ATTRIBUTE_ASSIGNMENT_RESULTS = "WsGetAttributeAssignmentsResults"; + + private static final String J_RESULTS = "results"; + private static final String J_STEM_RESULTS = "stemResults"; + private static final String J_GROUP_RESULTS = "groupResults"; + private static final String J_WS_GROUP_LOOKUPS = "wsGroupLookups"; + private static final String J_RESULT_METADATA = "resultMetadata"; + private static final String J_RESULT_CODE = "resultCode"; + private static final String J_SUCCESS = "success"; + private static final String J_WS_ATTRIBUTE_ASSIGN_TYPE = "attributeAssignType"; + + private static final String J_WS_SUBJECTS = "wsSubjects"; + private static final String J_WS_GROUP = "wsGroup"; + private static final String J_WS_GROUPS = "wsGroups"; + private static final String J_WS_GROUP_TYPE = "group"; + + private static final String J_UUID = "uuid"; + private static final String J_NAME = "name"; + private static final String J_EXTENSION = "extension"; + private static final String J_SOURCE_ID = "sourceId"; + private static final String J_ID = "id"; + private static final String J_PAGE_SIZE = "pageSize"; + private static final String J_PAGE_NUMBER = "pageNumber"; + private static final String J_ATTRIBUTE_NAME = "wsAttributeDefNameName"; + private static final String J_ATTRIBUTE_VALUE = "wsAttributeDefValueValue"; + + private static final String VAL_T = "T"; + private static final String VAL_FIND_BY_STEM_NAME = "FIND_BY_STEM_NAME"; + private static final String VAL_ALL_IN_SUBTREE = "ALL_IN_SUBTREE"; + + private static final String PATH_GROUPS = "/groups"; + private static final String PATH_STEMS = "/stems"; + private static final String PATH_ATTRIBUTES = "/attributeAssignments"; + + private static final String ATTR_EXTENSION = J_EXTENSION; + private static final String DEFAULT_BASE_STEM = ":"; + public static final String OBJECT_CLASS_NAME = "Group"; public static final String ATTR_NAME = "name"; public static final String ATTR_UUID = "uuid"; - private static final String ATTR_EXTENSION = J_EXTENSION; public static final String ATTR_MEMBER = "member"; - private static final String DEFAULT_BASE_STEM = ":"; + private final Map attributeNameValueMap = new HashMap<>(); + GroupProcessor(GrouperConfiguration configuration) { super(configuration); + + if (configuration.getGroupAttribute() != null && configuration.getGroupAttribute().length >0) { + attributeNameValueMap.putAll( + Arrays.stream(configuration.getGroupAttribute()) + .map(str -> str.split("\\|\\|")) + .collect(Collectors.toMap(str -> str[0].trim(), str -> str[1].trim())) + ); + } } ObjectClass getObjectClass() { @@ -53,8 +119,8 @@ ObjectClass getObjectClass() { void read(Filter filter, ResultsHandler handler, OperationOptions options) { if (filter == null) { getAllGroups(handler, options); - } else if (filter instanceof EqualsFilter) { - Attribute attribute = ((EqualsFilter) filter).getAttribute(); + } else if (filter instanceof EqualsFilter || filter instanceof ContainsFilter) { + Attribute attribute = ((AttributeFilter) filter).getAttribute(); if (attribute != null) { List values = attribute.getValue(); if (values == null || values.isEmpty()) { @@ -77,156 +143,287 @@ void read(Filter filter, ResultsHandler handler, OperationOptions options) { } } + private boolean executeGrouperRequest(final String name, final String uuid, final boolean withMembers, final boolean shouldPage, final ResultsHandler handler) { + final URIBuilder uriBuilder; + + if (attributeNameValueMap.isEmpty() || StringUtil.isNotBlank(name) || StringUtil.isNotBlank(uuid)) { + uriBuilder = getUriBuilderForGroups(); + } else { + uriBuilder = getUriBuilderForAttributes(); + } + + try { + final HttpPost request = new HttpPost(uriBuilder.build()); + final List requestBodies = createWsFindGroupsRequest(name, uuid, withMembers); + + final boolean paging = !requestBodies.get(0).has(J_WS_REST_ATTRIBUTE_ASSIGNMENT_LITE_REQUEST) && shouldPage; //TODO Grouper WS WsRestGetAttributeAssignmentsLiteRequest should really implement paging, if/when they do this can be removed! + + boolean result = false; + for (final JSONObject body : requestBodies) { + boolean localResult; + + if (withMembers) { + localResult = executeGetMembers(request, body, handler, paging); + } else { + localResult = executeFindGroups(request, body, handler, paging); + } + + if (!localResult) { + LOG.info("Problem processing/retrieving results for query {0}!", body.toString()); + } else { + result = true; + } + }; + + return result; + } catch (RuntimeException | URISyntaxException e) { + throw processException(e, uriBuilder, "Get group by name (with members)"); + } + } + private void getGroupByName(String name, ResultsHandler handler, OperationOptions options) { - URIBuilder uriBuilder = getUriBuilderForGroups(); if (!isGetMembers(options)) { - try { - HttpPost request = new HttpPost(uriBuilder.build()); - JSONObject body = new JSONObject() - .put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject() - .put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject() - .put(J_GROUP_NAME, name) })); - executeFindGroups(request, body, handler); - } catch (RuntimeException | URISyntaxException e) { - throw processException(e, uriBuilder, "Get group by name (no members)"); - } + LOG.info("Retrieving single group without membership by name..."); + executeGrouperRequest(name, null, false, false, handler); } else { - try { - HttpPost request = new HttpPost(uriBuilder.build()); - JSONObject body = new JSONObject() - .put(J_WS_REST_GET_MEMBERS_REQUEST, new JSONObject() - .put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject() - .put(J_GROUP_NAME, name) }) - .put(J_INCLUDE_SUBJECT_DETAIL, true)); - executeGetMembers(request, body, handler); - } catch (RuntimeException | URISyntaxException e) { - throw processException(e, uriBuilder, "Get group by name (with members)"); - } + LOG.info("Retrieving single group with membership by name..."); + executeGrouperRequest(name, null, true, true, handler); } } private boolean getGroupByUuid(String uuid, ResultsHandler handler, OperationOptions options) { if (!isGetMembers(options)) { - URIBuilder uriBuilder = getUriBuilderForGroups(); - try { - HttpPost request = new HttpPost(uriBuilder.build()); - JSONObject body = new JSONObject() - .put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject() - .put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject() - .put(J_UUID, uuid) })); - return executeFindGroups(request, body, handler); - } catch (RuntimeException | URISyntaxException e) { - throw processException(e, uriBuilder, "Get group by UUID (no members)"); - } + LOG.info("Retrieving single group without membership by UUID..."); + return executeGrouperRequest(null, uuid, false, false, handler); } else { - URIBuilder uriBuilder = getUriBuilderForGroups(); - try { - HttpPost request = new HttpPost(uriBuilder.build()); - JSONObject body = new JSONObject() - .put(J_WS_REST_GET_MEMBERS_REQUEST, new JSONObject() - .put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject() - .put(J_UUID, uuid) }) - .put(J_INCLUDE_SUBJECT_DETAIL, true)); - return executeGetMembers(request, body, handler); - } catch (RuntimeException | URISyntaxException e) { - throw processException(e, uriBuilder, "Get group by UUID (with members)"); - } + LOG.info("Retrieving single group with membership by UUID..."); + return executeGrouperRequest(null, uuid, true, true, handler); } } private void getAllGroups(final ResultsHandler handler, final OperationOptions options) { boolean getMembers = isGetMembers(options); if (!getMembers) { + LOG.info("Retrieving all groups without memberships..."); getAllGroupsNoMembers(handler); } else { + LOG.info("Retrieving all groups with memberships..."); ResultsHandler localHandler = connectorObject -> getGroupByUuid(connectorObject.getUid().getUidValue(), handler, options); getAllGroupsNoMembers(localHandler); } } private void getAllGroupsNoMembers(ResultsHandler handler) { - URIBuilder uriBuilder = getUriBuilderForGroups(); - try { - HttpPost request = new HttpPost(uriBuilder.build()); - String configuredBaseStem = configuration.getBaseStem(); - JSONObject body = new JSONObject() - .put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject() - .put(J_WS_QUERY_FILTER, new JSONObject() - .put(J_QUERY_FILTER_TYPE, VAL_FIND_BY_STEM_NAME) - .put(J_STEM_NAME, configuredBaseStem != null ? configuredBaseStem : DEFAULT_BASE_STEM) - .put(J_STEM_NAME_SCOPE, VAL_ALL_IN_SUBTREE))); - executeFindGroups(request, body, handler); - } catch (RuntimeException | URISyntaxException e) { - throw processException(e, uriBuilder, "Get all groups"); - } + executeGrouperRequest(null, null, false, true, handler); } - private boolean executeFindGroups(HttpPost request, JSONObject body, ResultsHandler handler) { - JSONObject response = callRequest(request, body, null).getResponse(); - checkSuccess(response, J_WS_FIND_GROUPS_RESULTS); - JSONArray groups = getArray(response, false, J_WS_FIND_GROUPS_RESULTS, J_GROUP_RESULTS); - if (groups != null) { - for (Object group : groups) { - if (!handleGroupJsonObject(group, handler)) { - return false; + private boolean executeFindGroups(HttpPost request, JSONObject body, ResultsHandler handler, boolean shouldPage) { + int pageNumber = 1; + int result = 0; + boolean done = !shouldPage; + + do { + addPageNumber(body, pageNumber, shouldPage); + final JSONObject response = callRequest(request, body, null).getResponse(); + + final List responseTypes = List.of(J_WS_FIND_GROUPS_RESULTS, J_WS_ATTRIBUTE_ASSIGNMENT_RESULTS); + checkSuccess(response, responseTypes); + + final JSONArray groups = getArray(response, false, responseTypes, List.of(J_GROUP_RESULTS, J_WS_GROUPS)); + + if (groups != null) { + for (Object group : groups) { + if (!handleGroupJsonObject(group, handler)) { + done = true; + break; + } + result++; } + pageNumber++; + + } else { + done = true; } - } - return true; + } while (!done); + + LOG.info("Found {0} group(s) in {1} pages!", result, pageNumber-1); + return result > 0; } - private boolean executeGetMembers(HttpPost request, JSONObject body, ResultsHandler handler) { - CallResponse callResponse = callRequest(request, body, (statusCode, responseBody) -> { - JSONObject errorResponse = new JSONObject(responseBody); - JSONObject resultMetadata = (JSONObject) getIfExists(errorResponse, J_WS_GET_MEMBERS_RESULTS, J_RESULTS, J_RESULT_METADATA); - String resultCode = resultMetadata != null ? getStringOrNull(resultMetadata, J_RESULT_CODE) : null; - boolean notFound = "GROUP_NOT_FOUND".equals(resultCode); - if (notFound) { - return CallResponse.error(responseBody); + private boolean executeGetMembers(HttpPost request, JSONObject body, ResultsHandler handler, boolean shouldPage) { + final List members = new ArrayList<>(); + boolean done = !shouldPage; + int pageNumber = 1; + ConnectorObjectBuilder builder = null; + String name; + + do { + addPageNumber(body, pageNumber, shouldPage); + final CallResponse callResponse = callRequest(request, body, (statusCode, responseBody) -> { + final JSONObject errorResponse = new JSONObject(responseBody); + final JSONObject resultMetadata = (JSONObject) getIfExists(errorResponse, List.of(J_WS_GET_MEMBERS_RESULTS, J_RESULTS), List.of(J_RESULT_METADATA)); + final String resultCode = resultMetadata != null ? getStringOrNull(resultMetadata, J_RESULT_CODE) : null; + boolean notFound = "GROUP_NOT_FOUND".equals(resultCode); + if (notFound) { + return CallResponse.error(responseBody); + } else { + return null; + } + }); + + if (!callResponse.isSuccess()) { + return true; + } + + final JSONObject response = callResponse.getResponse(); + checkSuccess(response, Collections.singletonList(J_WS_GET_MEMBERS_RESULTS)); + final JSONObject gObject = (JSONObject) get(response, List.of(J_WS_GET_MEMBERS_RESULTS, J_RESULTS), List.of(J_WS_GROUP)); + name = getStringOrNull(gObject, J_NAME); + + if (groupNameMatches(name)) { + if (builder == null) { + builder = startGroupObjectBuilding(gObject, name); + } + final JSONArray membersJsonArray = getArray(response, false, List.of(J_WS_GET_MEMBERS_RESULTS, J_RESULTS), List.of(J_WS_SUBJECTS)); + + if (membersJsonArray != null) { + for (Object memberObject : membersJsonArray) { + handleMemberJsonObject(memberObject, members); + } + pageNumber++; + + } else { + done = true; + } + } else { - return null; + break; } - }); + } while (!done); + LOG.info("Found {0} group member(s) in {1} pages for Group: {2}!", members.size(), pageNumber-1, name); + - if (!callResponse.isSuccess()) { - return true; + if (builder != null) { + builder.addAttribute(ATTR_MEMBER, members); + return handler.handle(builder.build()); } - JSONObject response = callResponse.getResponse(); - checkSuccess(response, J_WS_GET_MEMBERS_RESULTS); - JSONObject gObject = (JSONObject) get(response, J_WS_GET_MEMBERS_RESULTS, J_RESULTS, J_WS_GROUP); - String name = getStringOrNull(gObject, J_NAME); - if (groupNameMatches(name)) { - ConnectorObjectBuilder builder = startGroupObjectBuilding(gObject, name); - List members = new ArrayList<>(); - JSONArray membersJsonArray = getArray(response, false, J_WS_GET_MEMBERS_RESULTS, J_RESULTS, J_WS_SUBJECTS); - if (membersJsonArray != null) { - for (Object memberObject : membersJsonArray) { - JSONObject member = (JSONObject) memberObject; - String sourceId = getStringOrNull(member, J_SOURCE_ID); - if (sourceId == null || !sourceId.equals(configuration.getSubjectSource())) { - LOG.info("Skipping member with wrong source (e.g. one that is not a person) (source={0})", sourceId); - } else { - String subjectId = getStringOrNull(member, J_ID); - if (subjectId != null) { - members.add(subjectId); - } else { - LOG.info("Skipping unnamed member (source={0})", member); + return true; + } + + private List createWsFindGroupsRequest(final String name, final String uuid, final boolean withMembers) { + final JSONObject wsRequest = new JSONObject(); + + //TODO subject source filtering using WS Object! +// if (shouldCheckSubjectSource && (sourceId == null || !sourceId.equalsIgnoreCase(configuration.getSubjectSource()))) { +// LOG.info("Skipping member with wrong source (e.g. one that is not a person) (source={0})", sourceId); +// } else { +// +// } + + //TODO Group Name Filtering using WS Object +// private boolean groupNameMatches(String name) { +// if (name == null) { +// return false; +// } +// String[] includes = configuration.getGroupIncludePattern(); +// String[] excludes = configuration.getGroupExcludePattern(); +// return (includes == null || includes.length == 0 || groupNameMatches(name, includes)) && +// !groupNameMatches(name, excludes); +// } +// +// private boolean groupNameMatches(String name, String[] patterns) { +// if (patterns == null) { +// return false; +// } +// for (String pattern : patterns) { +// Pattern compiled = Pattern.compile(pattern); +// if (compiled.matcher(name).matches()) { +// return true; +// } +// } +// return false; +// } +// +// if (StringUtil.isNotBlank(configuration.getSubjectSource())) { +// shouldCheckSubjectSource = true; +// } +// +// if (StringUtil.isNotBlank(configuration.getBaseStem())) { +// shouldUseBaseStem = true; +// } + + + //Specific group or member requests + if (StringUtil.isNotBlank(uuid) && !withMembers) { + return List.of(new JSONObject().put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject() + .put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject() + .put(J_UUID, uuid) }))); + + } else if (StringUtil.isNotBlank(uuid)) { + return List.of(new JSONObject().put(J_WS_REST_GET_MEMBERS_REQUEST, new JSONObject() + .put(J_PAGE_SIZE, configuration.getPageSize()) + .put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject() + .put(J_UUID, uuid) }) + .put(J_INCLUDE_SUBJECT_DETAIL, true))); + } + + if (StringUtil.isNotBlank(name) && !withMembers) { + return List.of(new JSONObject().put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject() + .put(J_WS_GROUP_LOOKUPS, new JSONObject[]{new JSONObject() + .put(J_GROUP_NAME, name)}))); + + } else if (StringUtil.isNotBlank(name)) { + return List.of(new JSONObject().put(J_WS_REST_GET_MEMBERS_REQUEST, new JSONObject() + .put(J_PAGE_SIZE, configuration.getPageSize()) + .put(J_WS_GROUP_LOOKUPS, new JSONObject[] { new JSONObject() + .put(J_GROUP_NAME, name) }) + .put(J_INCLUDE_SUBJECT_DETAIL, true))); + } + + //Get All Requests + if (attributeNameValueMap.isEmpty()) { + return List.of(new JSONObject().put(J_WS_REST_FIND_GROUPS_REQUEST, new JSONObject() + .put(J_WS_QUERY_FILTER, new JSONObject() + .put(J_QUERY_FILTER_TYPE, VAL_FIND_BY_STEM_NAME) + .put(J_STEM_NAME, (configuration.getBaseStem() != null) ? configuration.getBaseStem() : DEFAULT_BASE_STEM) + .put(J_STEM_NAME_SCOPE, VAL_ALL_IN_SUBTREE) + .put(J_PAGE_SIZE, configuration.getPageSize())))); + } else { + return attributeNameValueMap.entrySet().stream().map(entry -> + new JSONObject().put(J_WS_REST_ATTRIBUTE_ASSIGNMENT_LITE_REQUEST, new JSONObject() + .put(J_WS_ATTRIBUTE_ASSIGN_TYPE, J_WS_GROUP_TYPE) + .put(J_ATTRIBUTE_NAME, entry.getKey()) + .put(J_ATTRIBUTE_VALUE, entry.getValue())) + ).collect(Collectors.toList()); + } + } + + private void addPageNumber(final JSONObject body, final int pageNumber, final boolean shouldPage) { + try { + if (shouldPage) { + if (body.has(J_WS_REST_GET_MEMBERS_REQUEST)) { + body.getJSONObject(J_WS_REST_GET_MEMBERS_REQUEST).put(J_PAGE_NUMBER, pageNumber); + } else if (body.has(J_WS_REST_FIND_GROUPS_REQUEST)) { + if (body.getJSONObject(J_WS_REST_FIND_GROUPS_REQUEST).has(J_WS_QUERY_FILTER)){ + final Object queryFilterType = body.getJSONObject(J_WS_REST_FIND_GROUPS_REQUEST).getJSONObject(J_WS_QUERY_FILTER).get(J_QUERY_FILTER_TYPE); + if (String.valueOf(queryFilterType).equalsIgnoreCase(VAL_FIND_BY_STEM_NAME)) { //Currently WS only supports paging on Group FIND by STEM or Approximate Group Name query types ONLY!! + body.getJSONObject(J_WS_REST_FIND_GROUPS_REQUEST).getJSONObject(J_WS_QUERY_FILTER).put(J_PAGE_NUMBER, pageNumber); } } } - builder.addAttribute(ATTR_MEMBER, members); } - return handler.handle(builder.build()); - } else { - return true; + } catch (JSONException e) { + LOG.info("Exception adding page number with {0}", e); + //swallow } } - private boolean handleGroupJsonObject(Object group, ResultsHandler handler) { + private boolean handleGroupJsonObject(final Object group, final ResultsHandler handler) { if (group instanceof JSONObject) { - JSONObject gObject = (JSONObject) group; - String name = getStringOrNull(gObject, J_NAME); + final JSONObject gObject = (JSONObject) group; + final String name = getStringOrNull(gObject, J_NAME); + if (groupNameMatches(name)) { return handler.handle(startGroupObjectBuilding(gObject, name).build()); } else { @@ -237,6 +434,26 @@ private boolean handleGroupJsonObject(Object group, ResultsHandler handler) { } } + private void handleMemberJsonObject(final Object memberObject, final List members) { + if (memberObject instanceof JSONObject) { + final JSONObject member = (JSONObject) memberObject; + final String sourceId = getStringOrNull(member, J_SOURCE_ID); + + if (sourceId == null || !sourceId.equals(configuration.getSubjectSource())) { + LOG.warn("Skipping member with wrong source (e.g. one that is not a person) (source={0})", sourceId); + } else { + final String subjectId = getStringOrNull(member, J_ID); + if (subjectId != null) { + members.add(subjectId); + } else { + LOG.warn("Skipping unnamed member (source={0})", member); + } + } + } else { + throw new IllegalStateException("Expected member as JSONObject, got " + memberObject); + } + } + private ConnectorObjectBuilder startGroupObjectBuilding(JSONObject gObject, String name) { String extension = getStringOrNull(gObject, J_EXTENSION); String uuid = getStringOrNull(gObject, J_UUID); @@ -248,16 +465,7 @@ private ConnectorObjectBuilder startGroupObjectBuilding(JSONObject gObject, Stri return builder; } - void test() { - if (configuration.getTestStem() != null) { - checkStemExists(configuration.getTestStem()); - } - if (configuration.getTestGroup() != null) { - checkGroupExists(configuration.getTestGroup()); - } - } - - private void checkStemExists(String stemName) { + private void checkStemExists(String stemName) { //TODO do we need to update/refactor this WS Query? URIBuilder uriBuilder = getUriBuilderForStems(); JSONArray stems; try { @@ -268,8 +476,8 @@ private void checkStemExists(String stemName) { .put(J_STEM_QUERY_FILTER_TYPE, VAL_FIND_BY_STEM_NAME) .put(J_STEM_NAME, stemName))); JSONObject response = callRequest(request, body, null).getResponse(); - checkSuccess(response, J_WS_FIND_STEMS_RESULTS); - stems = getArray(response, true, J_WS_FIND_STEMS_RESULTS, J_STEM_RESULTS); + checkSuccess(response, Collections.singletonList(J_WS_FIND_STEMS_RESULTS)); + stems = getArray(response, true, List.of(J_WS_FIND_STEMS_RESULTS), List.of(J_STEM_RESULTS)); } catch (RuntimeException | URISyntaxException e) { throw processException(e, uriBuilder, "Find stems request"); } @@ -281,13 +489,47 @@ private void checkStemExists(String stemName) { private void checkGroupExists(String groupName) { List groups = new ArrayList<>(); getGroupByName(groupName, groups::add, null); - LOG.info("getGroupByName found {0} group(s): {1}", groups.size(), groups); + + if (Boolean.TRUE.equals(configuration.getLogRequestResponses())) { + LOG.info("Get Group By Name found {0} group(s): {1}", groups.size(), groups); + } else { + LOG.info("Get Group By Name found {0} group(s).", groups.size()); + } + if (groups.isEmpty()) { - throw new ConnectorException("Expected to find the group '" + groupName + "', found none"); + throw new ConnectorException("Expected to find the group '" + groupName + "', but found none"); + } + } + + private boolean isGetMembers(OperationOptions options) { + String[] attrs = options != null ? options.getAttributesToGet() : null; + return attrs != null && Arrays.asList(attrs).contains(ATTR_MEMBER); + } + + private boolean groupNameMatches(String name) { + if (name == null) { + return false; } + String[] includes = configuration.getGroupIncludePattern(); + String[] excludes = configuration.getGroupExcludePattern(); + return (includes == null || includes.length == 0 || groupNameMatches(name, includes)) && + !groupNameMatches(name, excludes); } - ObjectClassInfoBuilder buildSchema() { + private boolean groupNameMatches(String name, String[] patterns) { + if (patterns == null) { + return false; + } + for (String pattern : patterns) { + Pattern compiled = Pattern.compile(pattern); + if (compiled.matcher(name).matches()) { + return true; + } + } + return false; + } + + public ObjectClassInfoBuilder buildSchema() { ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder(); builder.setType(OBJECT_CLASS_NAME); builder.addAttributeInfo( @@ -311,8 +553,37 @@ ObjectClassInfoBuilder buildSchema() { return builder; } - private boolean isGetMembers(OperationOptions options) { - String[] attrs = options != null ? options.getAttributesToGet() : null; - return attrs != null && Arrays.asList(attrs).contains(ATTR_MEMBER); + public void checkSuccess(final JSONObject response, final List rootNames) { + final JSONObject success = (JSONObject) get(response, rootNames, List.of(J_RESULT_METADATA)); + + if (!VAL_T.equals(success.get(J_SUCCESS))) { + throw new IllegalStateException("Request was not successful: " + success); + } + } + + public boolean isSuccess(JSONObject object) { + return VAL_T.equals(getStringOrNull(object, J_SUCCESS)); + } + + + public URIBuilder getUriBuilderForGroups() { + return getUriBuilderRelative(PATH_GROUPS); + } + + public URIBuilder getUriBuilderForStems() { + return getUriBuilderRelative(PATH_STEMS); + } + + public URIBuilder getUriBuilderForAttributes() { + return getUriBuilderRelative(PATH_ATTRIBUTES); + } + + public void test() { + if (configuration.getTestStem() != null) { + checkStemExists(configuration.getTestStem()); + } + if (configuration.getTestGroup() != null) { + checkGroupExists(configuration.getTestGroup()); + } } } diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java index e797d36..7c1da94 100644 --- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java @@ -22,8 +22,8 @@ import org.identityconnectors.framework.spi.AbstractConfiguration; import org.identityconnectors.framework.spi.ConfigurationProperty; import org.identityconnectors.framework.spi.StatefulConfiguration; - import java.util.Arrays; +import java.util.Map; /** * @author surmanek @@ -35,17 +35,27 @@ public class GrouperConfiguration extends AbstractConfiguration implements State private static final Log LOG = Log.getLog(GrouperConfiguration.class); + private static final String DEFAULT_CONTENT_TYPE_JSON = "application/json; charset=utf-8"; + private static final String DEFAULT_URI_BASE_PATH = "/grouper-ws/servicesRest/json/v2_4_000"; + private static final int DEFAULT_PAGE_SIZE = 100; + private String baseUrl; + private String uriBasePath; private String username; private GuardedString password; private Boolean ignoreSslValidation; + private String contentType; private String baseStem; private String[] groupIncludePattern; private String[] groupExcludePattern; + private String[] groupAttribute; private String subjectSource; private String testStem; private String testGroup; + private Integer pageSize; + private Boolean logRequestResponses; + @ConfigurationProperty(order = 10, displayMessageKey = "baseUrl.display", helpMessageKey = "baseUrl.help", required = true) public String getBaseUrl() { @@ -92,10 +102,86 @@ public void setIgnoreSslValidation(Boolean ignoreSslValidation) { this.ignoreSslValidation = ignoreSslValidation; } + /** + * Used to specify stem that is fetched during Test Connection (if any). + */ + @ConfigurationProperty(order = 50, displayMessageKey = "testStem.display", helpMessageKey = "testStem.help") + public String getTestStem() { + return testStem; + } + + public void setTestStem(String testStem) { + this.testStem = testStem; + } + + /** + * Used to specify group that is fetched during Test Connection (if any). + */ + @ConfigurationProperty(order = 60, displayMessageKey = "testGroup.display", helpMessageKey = "testGroup.help") + public String getTestGroup() { + return testGroup; + } + + public void setTestGroup(String testGroup) { + this.testGroup = testGroup; + } + + /** + * Used to specify page size for Grouper WS paging. + */ + @ConfigurationProperty(order = 70, displayMessageKey = "pageSize.display", helpMessageKey = "pageSize.help") + public Integer getPageSize() { + if (pageSize != null) { + return pageSize; + } else { + return DEFAULT_PAGE_SIZE; + } + } + + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + } + + /** + * Used in case the Grouper WS Base Path Changes + * @return + */ + @ConfigurationProperty(order = 80, displayMessageKey = "uriBasePath.display", helpMessageKey = "uriBasePath.help", required = false) + public String getUriBasePath() { + if (uriBasePath != null && !uriBasePath.isBlank()) { + return uriBasePath; + } else { + return DEFAULT_URI_BASE_PATH; + } + } + + public void setUriBasePath(String uriBasePath) { + this.uriBasePath = uriBasePath; + } + + /** + * Used in case Grouper WS Content Type Changes + * @return + */ + @ConfigurationProperty(order = 90, displayMessageKey = "contentType.display", helpMessageKey = "contentType.help", required = false) + public String getContentType() { + if (contentType != null && !contentType.isBlank()) { + return contentType; + } else { + return DEFAULT_CONTENT_TYPE_JSON; + } + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + + //TODO Implement the following in Grouper Query Filter /** * Used to specify root stem for groups returned by this connector. The default is ":" (the whole tree). */ - @ConfigurationProperty(order = 50, displayMessageKey = "baseStem.display", helpMessageKey = "baseStem.help") + @ConfigurationProperty(order = 100, displayMessageKey = "baseStem.display", helpMessageKey = "baseStem.help") public String getBaseStem() { return baseStem; } @@ -104,10 +190,22 @@ public void setBaseStem(String baseStem) { this.baseStem = baseStem; } + /** + * Which groups by attribute name/value should be visible to connector + */ + @ConfigurationProperty(order = 110, displayMessageKey = "groupAttribute.display", helpMessageKey = "groupAttribute.help") + public String[] getGroupAttribute() { + return groupAttribute; + } + + public void setGroupAttribute(String[] groupAttribute) { + this.groupAttribute = groupAttribute; + } + /** * Which groups should be visible to this connector? */ - @ConfigurationProperty(order = 60, displayMessageKey = "groupIncludePattern.display", helpMessageKey = "groupIncludePattern.help") + @ConfigurationProperty(order = 120, displayMessageKey = "groupIncludePattern.display", helpMessageKey = "groupIncludePattern.help") public String[] getGroupIncludePattern() { return groupIncludePattern; } @@ -119,7 +217,7 @@ public void setGroupIncludePattern(String[] groupIncludePattern) { /** * Which groups should be hidden (invisible) to this connector? */ - @ConfigurationProperty(order = 70, displayMessageKey = "groupExcludePattern.display", helpMessageKey = "groupExcludePattern.help") + @ConfigurationProperty(order = 130, displayMessageKey = "groupExcludePattern.display", helpMessageKey = "groupExcludePattern.help") public String[] getGroupExcludePattern() { return groupExcludePattern; } @@ -131,7 +229,7 @@ public void setGroupExcludePattern(String[] groupExcludePattern) { /** * Used to limit subjects returned by this connector. */ - @ConfigurationProperty(order = 80, displayMessageKey = "subjectSource.display", helpMessageKey = "subjectSource.help", required = true) + @ConfigurationProperty(order = 140, displayMessageKey = "subjectSource.display", helpMessageKey = "subjectSource.help") public String getSubjectSource() { return subjectSource; } @@ -141,28 +239,17 @@ public void setSubjectSource(String subjectSource) { } /** - * Used to specify stem that is fetched during Test Connection (if any). + * Should we log request/response logs to/from Grouper WS. */ - @ConfigurationProperty(order = 90, displayMessageKey = "testStem.display", helpMessageKey = "testStem.help") - public String getTestStem() { - return testStem; + @ConfigurationProperty(order = 160, displayMessageKey = "logRequestResponses.display", helpMessageKey = "logRequestResponses.help") + public Boolean getLogRequestResponses() { + return logRequestResponses; } - public void setTestStem(String testStem) { - this.testStem = testStem; + public void setLogRequestResponses(Boolean logRequestResponses) { + this.logRequestResponses = logRequestResponses; } - /** - * Used to specify group that is fetched during Test Connection (if any). - */ - @ConfigurationProperty(order = 100, displayMessageKey = "testGroup.display", helpMessageKey = "testGroup.help") - public String getTestGroup() { - return testGroup; - } - - public void setTestGroup(String testGroup) { - this.testGroup = testGroup; - } @Override public void validate() { @@ -173,8 +260,6 @@ public void validate() { exceptionMsg = "Name is not provided."; } else if (password == null) { exceptionMsg = "Password is not provided."; - } else if (subjectSource == null) { - exceptionMsg = "Subject source is not provided."; } else { return; } @@ -185,14 +270,20 @@ public void validate() { @Override public void release() { this.baseUrl = null; + this.uriBasePath = null; this.username = null; this.password = null; this.ignoreSslValidation = null; + this.contentType = null; this.baseStem = null; this.groupIncludePattern = null; this.groupExcludePattern = null; + this.groupAttribute = null; this.subjectSource = null; + this.testStem = null; this.testGroup = null; + this.pageSize = null; + this.logRequestResponses = null; } @Override @@ -205,7 +296,13 @@ public String toString() { ", groupIncludePattern=" + Arrays.toString(groupIncludePattern) + ", groupExcludePattern=" + Arrays.toString(groupExcludePattern) + ", subjectSource='" + subjectSource + '\'' + + ", testStem='" + testStem + '\'' + ", testGroup='" + testGroup + '\'' + + ", pageSize='" + pageSize + '\'' + + ", uriBasePath='" + uriBasePath + '\'' + + ", contentType='" + contentType + '\'' + + ", groupAttribute='" + groupAttribute + '\'' + + ", logRequestResponses='" + logRequestResponses + '\'' + '}'; } } diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java index 7025c29..09663ab 100644 --- a/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java @@ -26,71 +26,30 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.util.EntityUtils; +import org.identityconnectors.common.CollectionUtil; +import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.exceptions.*; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; - import java.io.*; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; -import java.util.regex.Pattern; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + /** - * Contains generic logic for handling REST operations over Grouper. + * Contains generic logic for handling REST operations. */ public class Processor { static final Log LOG = Log.getLog(GrouperConnector.class); - private static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8"; - - static final String J_WS_REST_GET_MEMBERS_REQUEST = "WsRestGetMembersRequest"; - static final String J_WS_REST_FIND_GROUPS_REQUEST = "WsRestFindGroupsRequest"; - static final String J_WS_REST_FIND_STEMS_REQUEST = "WsRestFindStemsRequest"; - - static final String J_WS_QUERY_FILTER = "wsQueryFilter"; - static final String J_WS_STEM_QUERY_FILTER = "wsStemQueryFilter"; - static final String J_STEM_QUERY_FILTER_TYPE = "stemQueryFilterType"; - static final String J_INCLUDE_SUBJECT_DETAIL = "includeSubjectDetail"; - static final String J_QUERY_FILTER_TYPE = "queryFilterType"; - static final String J_STEM_NAME = "stemName"; - static final String J_STEM_NAME_SCOPE = "stemNameScope"; - static final String J_GROUP_NAME = "groupName"; - - static final String J_WS_FIND_GROUPS_RESULTS = "WsFindGroupsResults"; - static final String J_WS_FIND_STEMS_RESULTS = "WsFindStemsResults"; - static final String J_WS_GET_MEMBERS_RESULTS = "WsGetMembersResults"; - - static final String J_RESULTS = "results"; - static final String J_STEM_RESULTS = "stemResults"; - static final String J_GROUP_RESULTS = "groupResults"; - static final String J_WS_GROUP_LOOKUPS = "wsGroupLookups"; - static final String J_RESULT_METADATA = "resultMetadata"; - static final String J_RESULT_CODE = "resultCode"; - private static final String J_SUCCESS = "success"; - - static final String J_WS_SUBJECTS = "wsSubjects"; - static final String J_WS_GROUP = "wsGroup"; - - static final String J_UUID = "uuid"; - static final String J_NAME = "name"; - static final String J_EXTENSION = "extension"; - static final String J_SOURCE_ID = "sourceId"; - static final String J_ID = "id"; - - private static final String VAL_T = "T"; - static final String VAL_FIND_BY_STEM_NAME = "FIND_BY_STEM_NAME"; - static final String VAL_ALL_IN_SUBTREE = "ALL_IN_SUBTREE"; - - private static final String URI_BASE_PATH = "/grouper-ws/servicesRest/json/v2_4_000"; - private static final String PATH_GROUPS = "/groups"; - private static final String PATH_STEMS = "/stems"; - GrouperConfiguration configuration; Processor(GrouperConfiguration configuration) { @@ -98,14 +57,26 @@ public class Processor { } CallResponse callRequest(HttpEntityEnclosingRequestBase request, JSONObject payload, ErrorHandler errorHandler) { - request.addHeader("Content-Type", Processor.CONTENT_TYPE_JSON); - request.addHeader("Authorization", "Basic " + getAuthEncoded()); + if (!request.containsHeader("Content-Type")) { + request.addHeader("Content-Type", configuration.getContentType()); + } + + if (!request.containsHeader("Authorization")) { + request.addHeader("Authorization", "Basic " + getAuthEncoded()); + } + request.setEntity(new ByteArrayEntity(payload.toString().getBytes(StandardCharsets.UTF_8))); - LOG.info("Payload: {0}", payload); // we don't log the whole request, as it contains the (encoded) password + + if (Boolean.TRUE.equals(configuration.getLogRequestResponses())) { + LOG.info("Payload: {0}", payload); // we don't log the whole request, as it contains the (encoded) password + } try (CloseableHttpResponse response = execute(request)) { - LOG.info("Response: {0}", response); + if (Boolean.TRUE.equals(configuration.getLogRequestResponses())) { + LOG.info("Response: {0}", response); + } + return processResponse(response, errorHandler); - } catch (IOException e) { + } catch (Exception e) { String msg = "Request failed: problem occurred during execute request with uri: " + request.getURI() + ": \n\t" + e.getLocalizedMessage(); LOG.error("{0}", msg); throw new ConnectorIOException(msg, e); @@ -143,7 +114,7 @@ private CloseableHttpResponse execute(HttpUriRequest request) { LOG.ok("response code: {0}", response.getStatusLine().getStatusCode()); // DO NOT CLOSE response HERE !!! return response; - } catch (IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + } catch (Exception e) { String msg = "Execution of the request failed: problem occurred during HTTP client execution: \n\t" + e.getLocalizedMessage(); LOG.error("{0}", msg, e); throw new ConnectorIOException(msg); @@ -158,7 +129,6 @@ private CloseableHttpResponse execute(HttpUriRequest request) { * @return true if the processing can continue */ private CallResponse processResponse(CloseableHttpResponse response, ErrorHandler errorHandler) throws IOException { - int statusCode = response.getStatusLine().getStatusCode(); LOG.info("Status code: {0}", statusCode); @@ -215,9 +185,7 @@ private CallResponse processResponse(CloseableHttpResponse response, ErrorHandle msg + ", exception: " + e.getMessage(), e); } } - throw new ConnectorException(msg); - } catch (Exception e) { LOG.error("{0}", msg); throw e; @@ -233,99 +201,177 @@ private void closeResponse(CloseableHttpResponse response) { } } - private URIBuilder getUriBuilderRelative(String path) { + public URIBuilder getUriBuilderRelative(String path) { try { URIBuilder uri = new URIBuilder(configuration.getBaseUrl()); - uri.setPath(URI_BASE_PATH + path); + uri.setPath(configuration.getUriBasePath() + path); return uri; } catch (URISyntaxException e) { throw new IllegalStateException(e.getMessage(), e); // todo } } - URIBuilder getUriBuilderForGroups() { - return getUriBuilderRelative(PATH_GROUPS); - } + @FunctionalInterface + public interface ErrorHandler { - URIBuilder getUriBuilderForStems() { - return getUriBuilderRelative(PATH_STEMS); + /** + * Returns null if the error couldn't be handled + */ + CallResponse handleError(int statusCode, String responseBody); } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + static class CallResponse { + private final boolean success; + private final JSONObject response; - void checkSuccess(JSONObject response, String rootName) { - Object success = get(response, rootName, J_RESULT_METADATA, J_SUCCESS); - if (!VAL_T.equals(success)) { - throw new IllegalStateException("Request was not successful: " + success); + private CallResponse(boolean success, JSONObject response) { + this.success = success; + this.response = response; + } + + static CallResponse ok(String text) { + return new CallResponse(true, new JSONObject(text)); + } + + static CallResponse error(String text) { + return new CallResponse(false, new JSONObject(text)); + } + + boolean isSuccess() { + return success; + } + + JSONObject getResponse() { + return response; } } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @SuppressWarnings("unused") - public Object getIfExists(JSONObject object, String... items) { - return get(object, false, items); + public Object getIfExists(final JSONObject object, final List possibleRootPathObjects, final List potentialObjectNamesToReturn) { + return get(object, false, possibleRootPathObjects, potentialObjectNamesToReturn); } - Object get(JSONObject object, String... items) { - return get(object, true, items); + Object get(final JSONObject object, final List possibleRootPathObjects, final List potentialObjectNamesToReturn) { + return get(object, true, possibleRootPathObjects, potentialObjectNamesToReturn); } - private Object get(JSONObject object, boolean mustExist, String... items) { - if (items.length == 0) { - throw new IllegalArgumentException("Empty item path"); + /** + * Used for parsing JSON response objects and returns a target JSONObject or JSONArray + * @param object + * @param possibleRootPathObjects + * @param potentialObjectNamesToFind + * @return + */ + private Object get(final JSONObject object, boolean mustExist, final List possibleRootPathObjects, final List potentialObjectNamesToFind) { + Object objectToReturn = null; + + if (potentialObjectNamesToFind.isEmpty()) { + throw new IllegalArgumentException("Empty item search, there is a problem with this connector!"); } - for (int i = 0; i < items.length - 1; i++) { - if (!object.has(items[i])) { - if (mustExist) { - throw new IllegalStateException("Item " + Arrays.asList(items).subList(0, i) + " was not found"); + + final String keyMatch = (possibleRootPathObjects != null) ? object.keySet().stream().filter(possibleRootPathObjects::contains).findFirst().orElse(null) : null; + if (mustExist && StringUtil.isBlank(keyMatch)) { + throw new IllegalStateException("Expected one of " + possibleRootPathObjects + "; but none were found in the JSON response!"); + } + + final List keysToCheck; + if (StringUtil.isNotBlank(keyMatch)) { + keysToCheck = List.of(keyMatch); + + } else { + keysToCheck = object.keySet().stream().filter(potentialObjectNamesToFind::contains).collect(Collectors.toList()); //response object has key or is a simple group object + + if (keysToCheck.isEmpty()) { + keysToCheck.addAll(object.keySet()); + } + } + + for (String key : keysToCheck) { + final Object keyObject = object.get(key); + + if (keyObject instanceof JSONObject) { + final JSONObject o = (JSONObject) keyObject; + final String match = o.keySet().stream().filter(potentialObjectNamesToFind::contains).findFirst().orElse(null); + + if (StringUtil.isNotBlank(match)) { + objectToReturn = o.get(match); //second level most desired JSON Objects are going to be found here + break; } else { - return null; + try { + final Object possible = get(o, false, possibleRootPathObjects, potentialObjectNamesToFind); //3rd level or more recursive + if (possible != null) { + objectToReturn = possible; + break; + } + } catch (JSONException e) { + //swallow + } } - } - Object o = object.get(items[i]); - if (o instanceof JSONArray) { - JSONArray array = (JSONArray) o; - if (array.length() == 0) { - if (mustExist) { - throw new IllegalStateException("Item " + Arrays.asList(items).subList(0, i) + " is an empty array"); - } else { - return null; + + } else if (keyObject instanceof JSONArray) { + final JSONArray o = (JSONArray) keyObject; //TODO not checking depth in JSON arrays at this time, just each element, is there a case in WS where this wouldn't work? + Object possible = null; + + for (int i = 0; i < o.length(); i++) { + try { + final String match = ((JSONObject) o.get(i)).keySet().stream() + .filter(potentialObjectNamesToFind::contains).findFirst().orElse(null); + if (match != null) { + possible = ((JSONObject) o.get(i)).get(match); + break; + } + } catch (JSONException e) { + //swallow } - } else if (array.length() > 1) { - throw new IllegalStateException("Item " + Arrays.asList(items).subList(0, i) + " is a multi-valued array (length: " + array.length() + ")"); - } else { - o = array.get(0); + } + + if (possible != null) { + objectToReturn = possible; + break; } } - if (o instanceof JSONObject) { - object = (JSONObject) o; + } + + //TODO the following can likely be refactored/removed if needed. + if (objectToReturn instanceof JSONArray) { + final JSONArray array = (JSONArray) objectToReturn; + if (array.length() == 0) { + if (mustExist) { + throw new IllegalStateException("Item " + objectToReturn + " is an empty array"); + } else { + return null; + } + // } else if (array.length() > 1) { + //throw new IllegalStateException("Item " + objectToReturn + " is a multi-valued array (length: " + array.length() + ")"); } else { - throw new IllegalStateException("Item " + Arrays.asList(items).subList(0, i) + " is neither object nor array; it is " + o.getClass()); + return array; } - } - String last = items[items.length - 1]; - if (object.has(last)) { - return object.get(last); - } else if (mustExist) { - throw new IllegalStateException("Item " + Arrays.asList(items) + " was not found"); - } else { + } else if (objectToReturn != null && !(objectToReturn instanceof JSONObject)) { + //throw new IllegalStateException("Item " + objectToReturn + " is neither object nor array; it is " + objectToReturn.getClass()); + LOG.warn("Item " + objectToReturn + " is neither object nor array; it is " + objectToReturn.getClass()); return null; } + + return objectToReturn; } @SuppressWarnings("unused") - JSONArray getArray(JSONObject object, String... items) { - return getArray(object, true, items); + JSONArray getArray(final JSONObject object, final List items, final List potentialArrayNames) { + return getArray(object, true, items, potentialArrayNames); } - JSONArray getArray(JSONObject object, boolean mustExist, String... items) { - Object rv = get(object, mustExist, items); + JSONArray getArray(final JSONObject object, boolean mustExist, final List rootItems, final List potentialArrayNames) { + final Object rv = get(object, mustExist, rootItems, potentialArrayNames); + if (rv == null) { assert !mustExist; return null; } else if (rv instanceof JSONArray) { return (JSONArray) rv; } else { - throw new IllegalStateException("Item " + Arrays.asList(items) + " should be an array but it's " + rv.getClass()); + throw new IllegalStateException("Item " + Arrays.asList(rootItems) + " should be an array but it's " + rv.getClass()); } } @@ -335,78 +381,11 @@ ConnectorException processException(Exception e, URIBuilder uriBuilder, final St return new ConnectorException(msg, e); } - @SuppressWarnings("unused") - public boolean isSuccess(JSONObject object) { - return VAL_T.equals(getStringOrNull(object, J_SUCCESS)); - } - - String getStringOrNull(JSONObject object, String item) { + String getStringOrNull(final JSONObject object, final String item) { if (object.has(item)) { - return getString(object, item); + return (String) object.get(item); //TODO any safety or other processing details needed here?!? } else { return null; } } - - private String getString(JSONObject object, String item) { - return (String) get(object, item); // todo error handling - } - - boolean groupNameMatches(String name) { - if (name == null) { - return false; - } - String[] includes = configuration.getGroupIncludePattern(); - String[] excludes = configuration.getGroupExcludePattern(); - return (includes == null || includes.length == 0 || groupNameMatches(name, includes)) && - !groupNameMatches(name, excludes); - } - - private boolean groupNameMatches(String name, String[] patterns) { - if (patterns == null) { - return false; - } - for (String pattern : patterns) { - Pattern compiled = Pattern.compile(pattern); - if (compiled.matcher(name).matches()) { - return true; - } - } - return false; - } - - @FunctionalInterface - public interface ErrorHandler { - - /** - * Returns null if the error couldn't be handled - */ - CallResponse handleError(int statusCode, String responseBody); - } - - static class CallResponse { - private final boolean success; - private final JSONObject response; - - private CallResponse(boolean success, JSONObject response) { - this.success = success; - this.response = response; - } - - static CallResponse ok(String text) { - return new CallResponse(true, new JSONObject(text)); - } - - static CallResponse error(String text) { - return new CallResponse(false, new JSONObject(text)); - } - - boolean isSuccess() { - return success; - } - - JSONObject getResponse() { - return response; - } - } } diff --git a/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties b/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties index 2c7bbf0..033239c 100644 --- a/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties +++ b/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties @@ -44,4 +44,19 @@ testStem.help=Stem whose accessibility is checked during Test connection operati testGroup.display=Test group testGroup.help=Group whose accessibility is checked during Test connection operation (if specified). +pageSize.display=Page Size +pageSize.help=Grouper WS Page Size parameter. Default is 100 which corresponds to 100 results. + +uriBasePath.display=Uri Base Path +uriBasePath.help=Grouper WS base path. Default is /grouper-ws/servicesRest/json/v2_4_000. + +contentType.display=Content Type +contentType.help=Grouper WS Content Type. Default is application/json; charset=utf-8. + +groupAttribute.display=Group Attribute Name/Value Map +groupAttribute.help=Group Attribute Name/Value Map. Key and Value are separated by ||. Example: attributeName||attributeValue + +logRequestResponses.display=Log Requests and Responses +logRequestResponses.help=Log in DEBUG Grouper WS requests and responses. Warning may create large log files! + GrouperConnector.rest.display=Grouper connector \ No newline at end of file diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/AbstractTest.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/AbstractTest.java index f69c9e0..d9d4b60 100644 --- a/src/test/java/com/evolveum/polygon/connector/grouper/test/AbstractTest.java +++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/AbstractTest.java @@ -48,6 +48,12 @@ class AbstractTest { private static final String[] GROUP_EXCLUDE_PATTERN = { ".*_(includes|excludes|systemOfRecord|systemOfRecordAndIncludes)" }; private static final String SUBJECT_SOURCE = "ldap"; private static final String TEST_STEM = ":"; + private static final Integer PAGE_SIZE = 5; + private static final String CONTENT_TYPE = "application/json; charset=utf-8"; + private static final String URI_BASE_PATH = "/grouper-ws/servicesRest/json/v2_4_000"; + private static final Boolean IGNORE_SSL_VALIDATION = true; + private static final Boolean LOG_REQUEST_RESPONSES = true; + private static final String GROUP_ATTRIBUTE = "control:attr:DeliverTo||midpoint"; final GrouperConnector grouperConnector = new GrouperConnector(); final OperationOptions options = new OperationOptions(new HashMap<>()); @@ -65,7 +71,7 @@ public void handleResult(SearchResult result) { } }; - GrouperConfiguration getConfiguration() { + GrouperConfiguration getConfigurationBaseStem() { GrouperConfiguration config = new GrouperConfiguration(); config.setBaseUrl(BASE_URL); config.setUsername(ADMIN_USERNAME); @@ -77,6 +83,31 @@ GrouperConfiguration getConfiguration() { config.setSubjectSource(SUBJECT_SOURCE); config.setTestStem(TEST_STEM); config.setTestGroup(TEST_GROUP); + config.setPageSize(PAGE_SIZE); + config.setUriBasePath(URI_BASE_PATH); + config.setContentType(CONTENT_TYPE); + config.setIgnoreSslValidation(IGNORE_SSL_VALIDATION); + config.setLogRequestResponses(LOG_REQUEST_RESPONSES); + return config; + } + + GrouperConfiguration getConfigurationAttributeFilter() { + GrouperConfiguration config = new GrouperConfiguration(); + config.setBaseUrl(BASE_URL); + config.setUsername(ADMIN_USERNAME); + config.setPassword(new GuardedString(ADMIN_PASSWORD.toCharArray())); + config.setIgnoreSslValidation(true); + config.setGroupIncludePattern(GROUP_INCLUDE_PATTERN); + config.setGroupExcludePattern(GROUP_EXCLUDE_PATTERN); + config.setSubjectSource(SUBJECT_SOURCE); + config.setTestStem(TEST_STEM); + config.setTestGroup(TEST_GROUP); + config.setPageSize(PAGE_SIZE); + config.setUriBasePath(URI_BASE_PATH); + config.setContentType(CONTENT_TYPE); + config.setIgnoreSslValidation(IGNORE_SSL_VALIDATION); + config.setLogRequestResponses(LOG_REQUEST_RESPONSES); + config.setGroupAttribute(new String[]{GROUP_ATTRIBUTE}); return config; } } diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTestAttributeFilter.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTestAttributeFilter.java new file mode 100644 index 0000000..11e102b --- /dev/null +++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTestAttributeFilter.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2019 Evolveum + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.evolveum.polygon.connector.grouper.test; + +import com.evolveum.polygon.connector.grouper.rest.GroupProcessor; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.AttributeFilter; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.identityconnectors.framework.common.objects.filter.FilterBuilder; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import static com.evolveum.polygon.connector.grouper.rest.GroupProcessor.ATTR_NAME; +import static com.evolveum.polygon.connector.grouper.rest.GroupProcessor.ATTR_UUID; +import static org.identityconnectors.framework.common.objects.OperationOptions.OP_ATTRIBUTES_TO_GET; +import static org.testng.AssertJUnit.assertEquals; + +/** + * Tests the group object class using attribute filter config. See the superclass for the environment needed. + */ +public class GroupTestAttributeFilter extends AbstractTest { + + private static final ObjectClass OC_GROUP = new ObjectClass(GroupProcessor.OBJECT_CLASS_NAME); + + private String uuid; + + @Test(priority = 1) + public void initialization() { + grouperConnector.init(getConfigurationAttributeFilter()); + } + + @Test(priority = 2) + public void testSchema() { + grouperConnector.schema(); + } + + @Test(priority = 3) + public void testTestOperation() { + grouperConnector.test(); + } + + @Test(priority = 4) + public void testFindByGroupName() { + results.clear(); + AttributeFilter filter = (EqualsFilter) FilterBuilder + .equalTo(AttributeBuilder.build(ATTR_NAME, TEST_GROUP)); + + grouperConnector.executeQuery(OC_GROUP, filter, handler, options); + assertEquals("Wrong # of groups retrieved", results.size(), 1); + ConnectorObject group = results.get(0); + System.out.println("Found group: " + group); + uuid = group.getUid().getUidValue(); + } + + @Test(priority = 10) + public void testFindByGroupNameWithMembers() { + results.clear(); + AttributeFilter filter = (EqualsFilter) FilterBuilder + .equalTo(AttributeBuilder.build(ATTR_NAME, TEST_GROUP)); + + grouperConnector.executeQuery(OC_GROUP, filter, handler, getMembersOptions()); + assertEquals("Wrong # of groups retrieved", results.size(), 1); + ConnectorObject group = results.get(0); + System.out.println("Found group: " + group); + List members = getMembers(group); + assertEquals("Wrong members", Collections.singletonList(TEST_USER), members); + } + + @Test(priority = 12) + public void testFindByGroupUuid() { + results.clear(); + AttributeFilter filter = (EqualsFilter) FilterBuilder + .equalTo(AttributeBuilder.build(ATTR_UUID, uuid)); + + grouperConnector.executeQuery(OC_GROUP, filter, handler, options); + assertEquals("Wrong # of groups retrieved", results.size(), 1); + ConnectorObject group = results.get(0); + System.out.println("Found group: " + group); + } + + @Test(priority = 13) + public void testFindByGroupUuidWihMembers() { + results.clear(); + AttributeFilter filter = (EqualsFilter) FilterBuilder + .equalTo(AttributeBuilder.build(ATTR_UUID, uuid)); + + grouperConnector.executeQuery(OC_GROUP, filter, handler, getMembersOptions()); + assertEquals("Wrong # of groups retrieved", results.size(), 1); + ConnectorObject group = results.get(0); + System.out.println("Found group: " + group); + assertEquals("Wrong members", Collections.singletonList(TEST_USER), getMembers(group)); + } + + @Test(priority = 14) + public void testGetAllGroups() { + results.clear(); + grouperConnector.executeQuery(OC_GROUP, null, handler, options); + for (ConnectorObject group : results) { + System.out.println("Found group: " + group); + } + } + + @Test(priority = 16) + public void testGetAllGroupsWithMembers() { + results.clear(); + grouperConnector.executeQuery(OC_GROUP, null, handler, getMembersOptions()); + for (ConnectorObject group : results) { + System.out.println("Found group: " + group); + } + } + + @Test(priority = 20) + public void dispose() { + grouperConnector.dispose(); + } + + private OperationOptions getMembersOptions() { + HashMap map = new HashMap<>(); + map.put(OP_ATTRIBUTES_TO_GET, new String[] { GroupProcessor.ATTR_MEMBER }); + return new OperationOptions(map); + } + + private List getMembers(ConnectorObject group) { + Attribute attribute = group.getAttributeByName(GroupProcessor.ATTR_MEMBER); + //noinspection unchecked + return attribute != null ? (List) (List) attribute.getValue() : Collections.emptyList(); + } +} \ No newline at end of file diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTest.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTestBaseStem.java similarity index 96% rename from src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTest.java rename to src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTestBaseStem.java index e6ac163..30a2c2c 100644 --- a/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTest.java +++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTestBaseStem.java @@ -22,20 +22,18 @@ import org.identityconnectors.framework.common.objects.filter.EqualsFilter; import org.identityconnectors.framework.common.objects.filter.FilterBuilder; import org.testng.annotations.Test; - import java.util.Collections; import java.util.HashMap; import java.util.List; - import static com.evolveum.polygon.connector.grouper.rest.GroupProcessor.ATTR_NAME; import static com.evolveum.polygon.connector.grouper.rest.GroupProcessor.ATTR_UUID; import static org.identityconnectors.framework.common.objects.OperationOptions.OP_ATTRIBUTES_TO_GET; import static org.testng.AssertJUnit.assertEquals; /** - * Tests the group object class. See the superclass for the environment needed. + * Tests the group object class using base stem config. See the superclass for the environment needed. */ -public class GroupTest extends AbstractTest { +public class GroupTestBaseStem extends AbstractTest { private static final ObjectClass OC_GROUP = new ObjectClass(GroupProcessor.OBJECT_CLASS_NAME); @@ -43,7 +41,7 @@ public class GroupTest extends AbstractTest { @Test(priority = 100) public void initialization() { - grouperConnector.init(getConfiguration()); + grouperConnector.init(getConfigurationBaseStem()); } @Test(priority = 110)