From 87ec525acf462f530e88d1af4a54cb3eb33c2dba Mon Sep 17 00:00:00 2001 From: Pavol Mederly Date: Mon, 18 Mar 2019 15:15:01 +0100 Subject: [PATCH] First version of the connector --- .gitignore | 22 + LICENSE | 201 +++ README.md | 1 + pom.xml | 136 ++ src/main/assembly/connector.xml | 47 + .../grouper/rest/AccountProcessor.java | 173 +++ .../grouper/rest/GroupProcessor.java | 167 +++ .../grouper/rest/GrouperConfiguration.java | 161 +++ .../grouper/rest/GrouperConnector.java | 142 ++ .../connector/grouper/rest/Processor.java | 1205 +++++++++++++++++ .../grouper/rest/Messages.properties | 8 + .../connector/grouper/test/AccountTests.java | 79 ++ .../connector/grouper/test/GroupTests.java | 94 ++ .../grouper/test/GrouperTestHelper.java | 102 ++ 14 files changed, 2538 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/assembly/connector.xml create mode 100644 src/main/java/com/evolveum/polygon/connector/grouper/rest/AccountProcessor.java create mode 100644 src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java create mode 100644 src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java create mode 100644 src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java create mode 100644 src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java create mode 100644 src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties create mode 100644 src/test/java/com/evolveum/polygon/connector/grouper/test/AccountTests.java create mode 100644 src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTests.java create mode 100644 src/test/java/com/evolveum/polygon/connector/grouper/test/GrouperTestHelper.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8aa7b14 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +.classpath +.project +.settings/ +scratch/ +test-output/ +target/ +src/test/java/com/evolveum/polygon/connector/jira/test/JiraConnectorSimpleTests +sample/ +.idea +*.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..958c9dc --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Grouper Rest Connector - documentation available at ... (TODO) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7cf04c6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,136 @@ + + + + 4.0.0 + + + connector-parent + com.evolveum.polygon + 1.4.2.14 + + + + connector-grouper-rest + 0.1 + jar + + Grouper REST Connector + + + com.evolveum.polygon.connector.grouper.rest + GrouperRestConnector + + + + + evolveum-nexus-releases + Internal Releases + http://nexus.evolveum.com/nexus/content/repositories/releases/ + + + evolveum-nexus-snapshots + Internal Releases + http://nexus.evolveum.com/nexus/content/repositories/snapshots/ + + + apache-snapshots + Apache Snapshots + http://repository.apache.org/snapshots/ + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + 2.19.1 + + + + + + + + connector-rest + com.evolveum.polygon + 1.4.2.14-SNAPSHOT + + + + org.apache.httpcomponents + httpclient + 4.5.1 + + + org.json + json + 20160810 + + + org.testng + testng + 6.8 + test + + + + batik + batik-swing + 1.6 + + + batik + batik-rasterizer + 1.6 + + + xml-apis + xml-apis + 1.3.04 + + + xml-apis + xml-apis-ext + 1.3.04 + + + + + diff --git a/src/main/assembly/connector.xml b/src/main/assembly/connector.xml new file mode 100644 index 0000000..efca6d1 --- /dev/null +++ b/src/main/assembly/connector.xml @@ -0,0 +1,47 @@ + + + + + connector + + + jar + + + false + + + + target/classes + + + + + + + lib + false + runtime + + net.tirasa.connid:connector-framework + + + + \ No newline at end of file diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/AccountProcessor.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/AccountProcessor.java new file mode 100644 index 0000000..c33afc4 --- /dev/null +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/AccountProcessor.java @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright 2017 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.rest; + +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.identityconnectors.framework.common.objects.filter.Filter; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import static com.evolveum.polygon.connector.grouper.rest.Processor.*; + +/** + * @author surmanek + * @author mederly + * + */ +public class AccountProcessor { + + private final Processor processor; + + public static final String ATTR_GROUP = "group"; + + public AccountProcessor(Processor processor) { + this.processor = processor; + } + + ObjectClassInfoBuilder buildSchema() { + ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder(); + AttributeInfoBuilder groups = new AttributeInfoBuilder(ATTR_GROUP, String.class); + groups.setMultiValued(true); + builder.addAttributeInfo(groups.build()); + return builder; + } + + void read(Filter filter, ResultsHandler handler, OperationOptions options) { + if (filter == null) { + getAllUsers(handler); + } else if (filter instanceof EqualsFilter && + (((EqualsFilter) filter).getAttribute() instanceof Name || ((EqualsFilter) filter).getAttribute() instanceof Uid)) { + Attribute name = ((EqualsFilter) filter).getAttribute(); + if (name != null) { + if (name.getValue() == null || name.getValue().isEmpty()) { + throw new IllegalArgumentException("No ID to look for"); + } else if (name.getValue().size() > 1) { + throw new IllegalArgumentException("More than one ID to look for: " + name.getValue()); + } else { + getUser((String) name.getValue().get(0), handler); + } + } else { + processor.throwNullAttrException(filter); + } + } else { + throw new IllegalArgumentException("Unsupported filter: " + filter); + } + } + + private void getAllUsers(ResultsHandler handler) { + URIBuilder uriBuilder = processor.getURIBuilder() + .setPath(URI_BASE_PATH + PATH_GROUPS); + try { + HttpPost request = new HttpPost(uriBuilder.build()); + JSONObject body = new JSONObject() + .put(WS_REST_GET_MEMBERS_REQUEST, new JSONObject() + .put(WS_GROUP_LOOKUPS, new JSONObject[] { + new JSONObject().put(GROUP_NAME, getConfiguration().getSuperGroup()) })); + System.out.println("Request = " + body.toString()); + JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON); + System.out.println("Got response: " + response); + processor.checkSuccess(response, WS_GET_MEMBERS_RESULTS); + JSONArray subjects = processor.getArray(response, WS_GET_MEMBERS_RESULTS, RESULTS, WS_SUBJECTS); + List ids = selectSubjectIds(subjects); + System.out.println("Subject IDs found: " + ids); + for (String id : ids) { + ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); + builder.setUid(id); + builder.setName(id); + AttributeBuilder groupBuilder = new AttributeBuilder().setName(ATTR_GROUP); + groupBuilder.setAttributeValueCompleteness(AttributeValueCompleteness.INCOMPLETE); + builder.addAttribute(groupBuilder.build()); + if (!handler.handle(builder.build())) { + return; + } + } + } catch (RuntimeException | URISyntaxException e) { + throw processor.processException(e, uriBuilder, "Get all users"); + } + } + + private List selectSubjectIds(JSONArray subjects) { + List rv = new ArrayList<>(subjects.length()); + for (Object subject : subjects) { + if (subject instanceof JSONObject) { + JSONObject subjObject = (JSONObject) subject; + if (processor.isSuccess(subjObject)) { + String sourceId = processor.getStringOrNull(subjObject, "sourceId"); + if (getConfiguration().getSubjectSource().equals(sourceId)) { + rv.add(processor.getString(subjObject, "id")); + } + } else { + LOG.warn("Skipping not-success subject from response: {}", subject); + } + } else { + throw new IllegalStateException("Expected subject as JSONObject, got " + subject); + } + } + return rv; + } + + private void getUser(String id, ResultsHandler handler) { + URIBuilder uriBuilder = processor.getURIBuilder() + .setPath(URI_BASE_PATH + PATH_SUBJECTS); + try { + HttpPost request = new HttpPost(uriBuilder.build()); + JSONObject body = new JSONObject() + .put("WsRestGetGroupsRequest", new JSONObject() + .put("subjectLookups", new JSONObject[] { + new JSONObject().put("subjectId", id) })); + System.out.println("Request = " + body.toString()); + JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON); + System.out.println("Got response: " + response); + processor.checkSuccess(response, WS_GET_GROUPS_RESULTS); + JSONArray groups = processor.getArray(response, WS_GET_GROUPS_RESULTS, RESULTS, WS_GROUPS); + + ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); + builder.setUid(id); + builder.setName(id); + builder.addAttribute(ATTR_GROUP, selectGroupNames(groups)); + handler.handle(builder.build()); + } catch (RuntimeException | URISyntaxException e) { + throw processor.processException(e, uriBuilder, "Get all users"); + } + } + + private List selectGroupNames(JSONArray groups) { + List rv = new ArrayList<>(); + String expectedPrefix = getConfiguration().getRootStem() + ":"; + for (Object group : groups) { + if (group instanceof JSONObject) { + JSONObject gObject = (JSONObject) group; + String name = processor.getStringOrNull(gObject, "name"); + String extension = processor.getStringOrNull(gObject, "extension"); + if (name != null && name.equals(expectedPrefix + extension)) { + rv.add(name); + } + } else { + throw new IllegalStateException("Expected group as JSONObject, got " + group); + } + } + return rv; + } + + private GrouperConfiguration getConfiguration() { + return processor.configuration; + } +} 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 new file mode 100644 index 0000000..9e4f6c1 --- /dev/null +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GroupProcessor.java @@ -0,0 +1,167 @@ +/******************************************************************************* + * Copyright 2017 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.rest; + +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.identityconnectors.framework.common.objects.filter.Filter; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.net.URISyntaxException; + +import static com.evolveum.polygon.connector.grouper.rest.Processor.*; + +/** + * @author surmanek + * @author mederly + * + */ +public class GroupProcessor { + + private final Processor processor; + + private static final String ATTR_EXTENSION = "extension"; + + public GroupProcessor(Processor processor) { + this.processor = processor; + } + + ObjectClassInfoBuilder buildSchema() { + ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder(); + + builder.setType(ObjectClass.GROUP_NAME); + AttributeInfoBuilder extension = new AttributeInfoBuilder(ATTR_EXTENSION, String.class); + builder.addAttributeInfo(extension.build()); + + return builder; + } + + void read(Filter filter, ResultsHandler handler, OperationOptions options) { + if (filter == null) { + getAllGroups(handler); + } else if (filter instanceof EqualsFilter && ((EqualsFilter) filter).getAttribute() instanceof Name) { + Attribute name = ((EqualsFilter) filter).getAttribute(); + if (name != null) { + if (name.getValue() == null || name.getValue().isEmpty()) { + throw new IllegalArgumentException("No group name to look for"); + } else if (name.getValue().size() > 1) { + throw new IllegalArgumentException("More than one group name to look for: " + name.getValue()); + } else { + getGroupByName((String) name.getValue().get(0), handler); + } + } else { + processor.throwNullAttrException(filter); + } + } else if (filter instanceof EqualsFilter && ((EqualsFilter) filter).getAttribute() instanceof Uid) { + Attribute name = ((EqualsFilter) filter).getAttribute(); + if (name != null) { + if (name.getValue() == null || name.getValue().isEmpty()) { + throw new IllegalArgumentException("No group UUID to look for"); + } else if (name.getValue().size() > 1) { + throw new IllegalArgumentException("More than one group UUID to look for: " + name.getValue()); + } else { + getGroupByUuid((String) name.getValue().get(0), handler); + } + } else { + processor.throwNullAttrException(filter); + } + } else { + throw new IllegalArgumentException("Unsupported filter: " + filter); + } + } + + private void getAllGroups(ResultsHandler handler) { + URIBuilder uriBuilder = processor.getURIBuilder() + .setPath(URI_BASE_PATH + PATH_GROUPS); + try { + HttpPost request = new HttpPost(uriBuilder.build()); + JSONObject body = new JSONObject() + .put("WsRestFindGroupsRequest", new JSONObject() + .put("wsQueryFilter", new JSONObject() + .put("queryFilterType", "FIND_BY_STEM_NAME") + .put("stemName", getConfiguration().getRootStem()))); + executeFindGroupsResponse(request, body, handler); + } catch (RuntimeException | URISyntaxException e) { + throw processor.processException(e, uriBuilder, "Get all groups"); + } + } + + private void executeFindGroupsResponse(HttpPost request, JSONObject body, ResultsHandler handler) { + System.out.println("Request = " + body.toString()); + JSONObject response = processor.callRequest(request, body, true, CONTENT_TYPE_JSON); + System.out.println("Got response: " + response); + processor.checkSuccess(response, "WsFindGroupsResults"); + JSONArray groups = processor.getArray(response, "WsFindGroupsResults", "groupResults"); + for (Object group : groups) { + if (!handlerGroupJsonObject(group, handler)) { + return; + } + } + } + + private void getGroupByName(String groupName, ResultsHandler handler) { + URIBuilder uriBuilder = processor.getURIBuilder() + .setPath(URI_BASE_PATH + PATH_GROUPS); + try { + HttpPost request = new HttpPost(uriBuilder.build()); + JSONObject body = new JSONObject() + .put("WsRestFindGroupsRequest", new JSONObject() + .put("wsGroupLookups", new JSONObject[] { new JSONObject() + .put("groupName", groupName) })); + executeFindGroupsResponse(request, body, handler); + } catch (RuntimeException | URISyntaxException e) { + throw processor.processException(e, uriBuilder, "Get all groups"); + } + } + + private void getGroupByUuid(String groupUuid, ResultsHandler handler) { + URIBuilder uriBuilder = processor.getURIBuilder() + .setPath(URI_BASE_PATH + PATH_GROUPS); + try { + HttpPost request = new HttpPost(uriBuilder.build()); + JSONObject body = new JSONObject() + .put("WsRestFindGroupsRequest", new JSONObject() + .put("wsGroupLookups", new JSONObject[] { new JSONObject() + .put("uuid", groupUuid) })); + executeFindGroupsResponse(request, body, handler); + } catch (RuntimeException | URISyntaxException e) { + throw processor.processException(e, uriBuilder, "Get all groups"); + } + } + + private boolean handlerGroupJsonObject(Object group, ResultsHandler handler) { + if (group instanceof JSONObject) { + JSONObject gObject = (JSONObject) group; + String name = processor.getStringOrNull(gObject, "name"); + String extension = processor.getStringOrNull(gObject, "extension"); + String uuid = processor.getStringOrNull(gObject, "uuid"); + ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); + builder.setObjectClass(ObjectClass.GROUP); + builder.setUid(uuid); + builder.setName(name); + builder.addAttribute(ATTR_EXTENSION, extension); + return handler.handle(builder.build()); + } else { + throw new IllegalStateException("Expected group as JSONObject, got " + group); + } + } + + private GrouperConfiguration getConfiguration() { + return processor.configuration; + } + +} 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 new file mode 100644 index 0000000..ffa0e5d --- /dev/null +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConfiguration.java @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2016 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.rest; + +import org.identityconnectors.common.StringUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.exceptions.ConfigurationException; +import org.identityconnectors.framework.spi.AbstractConfiguration; +import org.identityconnectors.framework.spi.ConfigurationProperty; +import org.identityconnectors.framework.spi.StatefulConfiguration; + +/** + * @author surmanek + * @author mederly + * + */ +public class GrouperConfiguration extends AbstractConfiguration implements StatefulConfiguration { + + private static final Log LOG = Log.getLog(GrouperConfiguration.class); + + private String name; + private GuardedString password; + private String baseUrl; + private String superGroup; + private String rootStem; + private Boolean ignoreSslValidation; + private String subjectSource; + + // getter and setter methods for "baseUrl" attribute: + @ConfigurationProperty(order = 1, displayMessageKey = "baseUrl.display", helpMessageKey = "baseUrl.help", required = true) + public String getBaseUrl() { + return baseUrl; + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + // getter and setter methods for "name" attribute: + @ConfigurationProperty(order = 2, displayMessageKey = "username.display", helpMessageKey = "username.help", required = true) + public String getUsername() { + return name; + } + + public void setUsername(String name) { + this.name = name; + } + + private String stringPassword = ""; + @ConfigurationProperty(order = 3, displayMessageKey = "password.display", helpMessageKey = "password.help", required = true, confidential = false) + public GuardedString getPassword() { + return password; + } + public void setPassword(GuardedString password) { + this.password = password; + } + + public String getStringPassword() { + password.access(new GuardedString.Accessor() { + @Override + public void access(char[] clearChars) { + stringPassword = new String(clearChars); + } + }); + return stringPassword; + } + + @ConfigurationProperty(order = 4, displayMessageKey = "superGroup.display", helpMessageKey = "superGroup.help", required = true) + public String getSuperGroup() { + return superGroup; + } + + public void setSuperGroup(String superGroup) { + this.superGroup = superGroup; + } + + @ConfigurationProperty(order = 5, displayMessageKey = "rootStem.display", helpMessageKey = "superGroup.help", required = true) + public String getRootStem() { + return rootStem; + } + + public void setRootStem(String rootStem) { + this.rootStem = rootStem; + } + + @ConfigurationProperty(order = 6, displayMessageKey = "ignoreSslValidation.display", helpMessageKey = "ignoreSslValidation.help", required = false) + public Boolean getIgnoreSslValidation() { + return ignoreSslValidation; + } + + public void setIgnoreSslValidation(Boolean ignoreSslValidation) { + this.ignoreSslValidation = ignoreSslValidation; + } + + @ConfigurationProperty(order = 7, displayMessageKey = "subjectSource.display", helpMessageKey = "subjectSource.help", required = false) + public String getSubjectSource() { + return subjectSource; + } + + public void setSubjectSource(String subjectSource) { + this.subjectSource = subjectSource; + } + + @Override + public void validate() { + String exceptionMsg; + if (baseUrl == null || StringUtil.isBlank(baseUrl)) { + exceptionMsg = "Base url is not provided."; + } else if (name == null || StringUtil.isBlank(name)) { + exceptionMsg = "Name is not provided."; + } else if (password == null) { + exceptionMsg = "Password is not provided."; + } else if (superGroup == null) { + exceptionMsg = "Super group is not provided."; + } else if (rootStem == null) { + exceptionMsg = "Root stem is not provided."; + } else if (subjectSource == null) { + exceptionMsg = "Subject source is not provided."; + } else { + return; + } + LOG.error(exceptionMsg); + throw new ConfigurationException(exceptionMsg); + } + + @Override + public void release() { + LOG.info("The release of configuration resources is being performed"); + this.password = null; + this.name = null; + this.baseUrl = null; + this.superGroup = null; + this.rootStem = null; + } + + @Override + public String toString() { + return "GrouperConfiguration{" + + "username='" + name + '\'' + + ", baseUrl='" + baseUrl + '\'' + + ", superGroup='" + superGroup + '\'' + + ", rootStem='" + rootStem + '\'' + + ", ignoreSslValidation='" + ignoreSslValidation + '\'' + + '}'; + } + +} diff --git a/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java new file mode 100644 index 0000000..8f63944 --- /dev/null +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/GrouperConnector.java @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2017 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.rest; + +import org.identityconnectors.common.CollectionUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.ConfigurationException; +import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.*; +import org.identityconnectors.framework.spi.Configuration; +import org.identityconnectors.framework.spi.Connector; +import org.identityconnectors.framework.spi.ConnectorClass; +import org.identityconnectors.framework.spi.operations.*; + +import java.util.List; + +/** + * @author surmanek + * @author mederly + * + */ +@ConnectorClass(displayNameKey = "GrouperConnector.rest.display", configurationClass = GrouperConfiguration.class) + +public class GrouperConnector implements TestOp, SchemaOp, Connector, SearchOp { + + private static final Log LOG = Log.getLog(GrouperConnector.class); + private GrouperConfiguration configuration; + private Processor processor; + private AccountProcessor accountProcessor; + private GroupProcessor groupProcessor; + + private static final String PROJECT_NAME = "PROJECT"; + private static final String ATTR_GROUPS = "groups"; + + @Override + public GrouperConfiguration getConfiguration() { + return configuration; + } + + @Override + public void init(Configuration configuration) { + if (configuration == null) { + LOG.error("Initialization of the configuration failed: Configuration is not provided."); + throw new ConfigurationException( + "Initialization of the configuration failed: Configuration is not provided."); + } + this.configuration = (GrouperConfiguration) configuration; + this.configuration.validate(); + this.processor = new Processor(this.configuration); + this.accountProcessor = new AccountProcessor(processor); + this.groupProcessor = new GroupProcessor(processor); + } + + @Override + public void dispose() { + configuration = null; + processor = null; + accountProcessor = null; + groupProcessor = null; + } + + @Override + public void test() { + LOG.info("Testing connection..."); + processor.test(); + LOG.ok("Testing finished successfully."); + } + + @Override + public Schema schema() { + SchemaBuilder schemaBuilder = new SchemaBuilder(GrouperConnector.class); + + // build user schema: + AccountProcessor user = new AccountProcessor(processor); + ObjectClassInfoBuilder userBuilder = user.buildSchema(); + schemaBuilder.defineObjectClass(userBuilder.build()); + + // build group schema: + GroupProcessor group = new GroupProcessor(processor); + ObjectClassInfoBuilder groupBuilder = group.buildSchema(); + schemaBuilder.defineObjectClass(groupBuilder.build()); + + return schemaBuilder.build(); + } + + @Override + public FilterTranslator createFilterTranslator(ObjectClass arg0, OperationOptions arg1) { + return new FilterTranslator() { + @Override + public List translate(Filter filter) { + return CollectionUtil.newList(filter); + } + }; + } + + @Override + public void executeQuery(ObjectClass objClass, Filter filter, ResultsHandler handler, OperationOptions options) { + LOG.info("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Execute Query-Parameters~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + if (objClass == null) { + LOG.error("Get operation failed: Attribute Object Class is not provided."); + throw new InvalidAttributeValueException("Attribute Object Class is not provided."); + } else + LOG.info("ObjectClasss: {0}", objClass.toString()); + + if (handler == null) { + LOG.error("Get operation failed: Attribute Result Handler is not provided."); + throw new InvalidAttributeValueException("Attribute Result Handler is not provided."); + } else + LOG.info("Execute Query-Handler: {0}", handler.toString()); + + if (options == null) { + LOG.error("Get operation failed: Attribute Options is not provided."); + throw new InvalidAttributeValueException("Attribute Options is not provided."); + } else + LOG.info("Options: {0}", options.toString()); + + LOG.info("Filter: {0}", filter); + LOG.info("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); + + if (objClass.is(ObjectClass.ACCOUNT_NAME)) { + accountProcessor.read(filter, handler, options); + } else if (objClass.is(ObjectClass.GROUP_NAME)) { + groupProcessor.read(filter, handler, options); + } + } + + +} 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 new file mode 100644 index 0000000..f41c7b9 --- /dev/null +++ b/src/main/java/com/evolveum/polygon/connector/grouper/rest/Processor.java @@ -0,0 +1,1205 @@ +/******************************************************************************* + * Copyright 2017 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.rest; + +import org.apache.batik.transcoder.TranscoderException; +import org.apache.batik.transcoder.TranscoderInput; +import org.apache.batik.transcoder.TranscoderOutput; +import org.apache.batik.transcoder.image.JPEGTranscoder; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.client.methods.*; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustStrategy; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.util.EntityUtils; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.exceptions.*; +import org.identityconnectors.framework.common.objects.*; +import org.identityconnectors.framework.common.objects.filter.*; +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * @author surmanek + * + */ +public class Processor { + + static final Log LOG = Log.getLog(GrouperConnector.class); + static final String CONTENT_TYPE_JSON = "application/json; charset=utf-8"; + public static final String WS_FIND_STEMS_RESULTS = "WsFindStemsResults"; + public static final String RESULT_METADATA = "resultMetadata"; + public static final String SUCCESS = "success"; + public static final String STEM_RESULTS = "stemResults"; + public static final String PATH_STEMS = "/stems"; + public static final String WS_GET_MEMBERS_RESULTS = "WsGetMembersResults"; + public static final String RESULTS = "results"; + public static final String WS_SUBJECTS = "wsSubjects"; + public static final String WS_REST_GET_MEMBERS_REQUEST = "WsRestGetMembersRequest"; + public static final String WS_GROUP_LOOKUPS = "wsGroupLookups"; + public static final String GROUP_NAME = "groupName"; + public static final String WS_GET_GROUPS_RESULTS = "WsGetGroupsResults"; + public static final String WS_GROUPS = "wsGroups"; + GrouperConfiguration configuration; + + public static final String URI_BASE_PATH = "/grouper-ws/servicesRest/json/v2_4_000"; + public static final String PATH_GROUPS = "/groups"; + public static final String PATH_SUBJECTS = "/subjects"; + + + static final String URI_USER_PATH = "/user"; + static final String URI_SEARCH_PATH = "/search"; + static final String PARAM_USERNAME = "username"; + static final String PARAM_START_AT = "startAt"; + static final String PARAM_MAX_RESULTS = "maxResults"; + static final String ATTR_GROUPS = "groups"; + static final String ATTR_AVATAR_URLS = "avatarUrls"; + + static final String USER_NAME = "USER"; + static final String PROJECT_NAME = "PROJECT"; + + static final String URI_PASSWORD_PATH = "/password"; + static final String URI_TEMP_AVATAR_PATH = "/avatar/temporary"; + static final String URI_AVATAR_PATH = "/avatar"; + static final String URI_GROUP_PATH = "/group"; + static final String URI_GROUPS_PICKER_PATH = "/groups/picker"; + static final String URI_PROJECT_PATH = "/project"; + + static final String PARAM_FILENAME = "filename"; + static final String PARAM_KEY = "key"; + static final String CONTENT_TYPE_JPEG_IMAGE = "image/jpeg"; + static final String UID = "key"; + + // project: + static final String ATTR_DEVELOPERS_GROUPS = "Developers.groups"; + static final String ATTR_DEVELOPERS_USERS = "Developers.users"; + static final String ATTR_ADMINISTRATORS_GROUPS = "Administrators.groups"; + static final String ATTR_ADMINISTRATORS_USERS = "Administrators.users"; + static final String ATTR_ACTOR_USER = "user"; + static final String ATTR_ACTOR_GROUP = "group"; + static final String ATTR_DEVELOPERS = "Developers"; + static final String ATTR_ADMINISTRATORS = "Administrators"; + + // user+project: + static final String ATTR_KEY = "key"; + static final String ATTR_AVATAR_BYTE_ARRRAY = "binaryAvatar"; + // user+group: + static final String ATTR_SELF = "self"; + static final String ATTR_NAME = "name"; + static final String ATTR_EXPAND = "expand"; + + static final String MIDPOINT_NAME = "__NAME__"; + static final boolean IS_MULTI_VALUE = true; + static final boolean IS_SINGLE_VALUE = false; + static final String EXTENDED_ATTR_NAME = "name"; + static final String EXTENDED_ATTR_ITEMS = "items"; + + public Processor(GrouperConfiguration configuration) { + this.configuration = configuration; + } + + + //put objects of array1 to the end of array2 + private JSONArray concatJSONArrays(JSONArray array1, JSONArray array2){ + for (Object obj : array1){ + array2.put(obj); + } + return array2; + } + + + JSONObject callRequest(HttpEntityEnclosingRequestBase request, JSONObject jo, Boolean parseResult, + String contentType) { + // don't log request here - password field !!! + if (contentType != null) + request.addHeader("Content-Type", contentType); + request.addHeader("Authorization", "Basic " + authEncoding()); + HttpEntity entity; + try { + entity = new ByteArrayEntity(jo.toString().getBytes("UTF-8")); + } catch (UnsupportedEncodingException e1) { + String exceptionMsg = "Creating request entity failed: problem occured during entity encoding."; + LOG.error(exceptionMsg); + throw new ConnectorIOException(exceptionMsg); + } + request.setEntity(entity); + try (CloseableHttpResponse response = execute(request)){ + + + + //LOG.ok("Request: {0}", request.toString()); + //response = execute(request); + LOG.ok("Response: {0}", response); + processResponseErrors(response); + + if (!parseResult) { + return null; + } + + String result = EntityUtils.toString(response.getEntity()); + + LOG.ok("Response body: {0}", result); + return new JSONObject(result); + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Request failed: problem occured during execute request with uri: ") + .append(request.getURI()).append(": \n\t").append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString(), e); + } + } + + JSONObject callRequest(HttpRequestBase request, Boolean parseResult, String contentType) { + // don't log request here - password field !!! + //CloseableHttpResponse response = null; + LOG.ok("request URI: {0}", request.getURI()); + request.addHeader("Content-Type", contentType); + request.addHeader("Authorization", "Basic " + authEncoding()); + try (CloseableHttpResponse response = execute(request)){ + + LOG.ok("Response: {0}", response); + processResponseErrors(response); + + if (!parseResult) { + //closeResponse(response); + return null; + } + // DO NOT USE getEntity() TWICE!!! + String result = EntityUtils.toString(response.getEntity()); + //closeResponse(response); + LOG.ok("Response body: {0}", result); + return new JSONObject(result); + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Request failed: problem occured during execute request with uri: ") + .append(request.getURI()).append(": \n\t").append(e.getLocalizedMessage()); + //closeResponse(response); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString(), e); + } + } + + JSONArray callRequest(HttpRequestBase request) { + //CloseableHttpResponse response = null; + LOG.ok("request URI: {0}", request.getURI()); + request.addHeader("Content-Type", CONTENT_TYPE_JSON); + request.addHeader("Authorization", "Basic " + authEncoding()); + try (CloseableHttpResponse response = execute(request)) { + + //response = execute(request); + LOG.ok("Response: {0}", response); + processResponseErrors(response); + // DO NOT USE getEntity() TWICE!!! + String result = EntityUtils.toString(response.getEntity()); + //closeResponse(response); + LOG.ok("Response body: {0}", result); + return new JSONArray(result); + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Request failed: problem occured during execute request with URI: ") + .append(request.getURI()).append(": \n\t").append(e.getLocalizedMessage()); + //closeResponse(response); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString(), e); + } + } + + String authEncoding() { + String username = configuration.getUsername(); + String password = configuration.getStringPassword(); + if (username == null || username.equals("")) { + LOG.error("Authentication failed: Username is not provided."); + throw new InvalidCredentialException("Authentication failed: Username is not provided."); + } + if (password == null || password.equals("")) { + LOG.error("Authentication failed: Password is not provided."); + throw new InvalidPasswordException("Authentication failed: Password is not provided."); + } + StringBuilder nameAndPasswd = new StringBuilder(); + nameAndPasswd.append(username).append(":").append(password); + // String nameAndPasswd = "administrator:training" + String encoding = Base64.encodeBase64String(nameAndPasswd.toString().getBytes()); + return encoding; + } + + CloseableHttpResponse execute(HttpUriRequest request) { + try { + HttpClientBuilder clientBuilder = HttpClientBuilder.create(); + if (Boolean.TRUE.equals(configuration.getIgnoreSslValidation())) { + SSLContextBuilder sslCtxBuilder = new SSLContextBuilder(); + sslCtxBuilder.loadTrustMaterial(null, new TrustStrategy() { + public boolean isTrusted(X509Certificate[] chain, String authType) { + return true; + } + }); + SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslCtxBuilder.build(), NoopHostnameVerifier.INSTANCE); + clientBuilder.setSSLSocketFactory(factory); + System.out.println("Ignoring SSL validation"); + } + CloseableHttpClient client = clientBuilder.build(); + CloseableHttpResponse response = client.execute(request); + // print response code: + LOG.ok("response code: {0}", String.valueOf(response.getStatusLine().getStatusCode())); + // client.close(); + // DO NOT CLOSE response HERE !!! + return response; + } catch (IOException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Execution of the request failed: problem occurred during HTTP client execution: \n\t") + .append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString(), e); + e.printStackTrace(); + throw new ConnectorIOException(exceptionMsg.toString()); + } + } + + /** + * Checks HTTP response for errors. If the response is an error then the + * method throws the ConnId exception that is the most appropriate match for + * the error. + */ + void processResponseErrors(CloseableHttpResponse response) { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode >= 200 && statusCode <= 299) { + return; + } + String responseBody = null; + try { + responseBody = EntityUtils.toString(response.getEntity()); + } catch (IOException e) { + LOG.warn("cannot read response body: " + e, e); + } + + StringBuilder message = new StringBuilder(); + message.append("HTTP error ").append(statusCode).append(" ").append(response.getStatusLine().getReasonPhrase()) + .append(" : ").append(responseBody); + if (statusCode == 401 || statusCode == 403) { + StringBuilder anauthorizedMessage = new StringBuilder(); // response + // body + // of + // status + // code + // 401 + // contains + // binary + // data. + anauthorizedMessage.append("HTTP error ").append(statusCode).append(" ") + .append(response.getStatusLine().getReasonPhrase()) + .append(" : Provided credentials are incorrect."); + closeResponse(response); + LOG.error("{0}", anauthorizedMessage.toString()); + throw new InvalidCredentialException(anauthorizedMessage.toString()); + } + LOG.error("{0}", message.toString()); + if ((statusCode == 400 || statusCode == 404) && message.toString().contains("already")) { + closeResponse(response); + LOG.error(message.toString()); + throw new AlreadyExistsException(message.toString()); + } + if (statusCode == 400 || statusCode == 405 || statusCode == 406) { + closeResponse(response); + LOG.error(message.toString()); + throw new ConnectorIOException(message.toString()); + } + if (statusCode == 402 || statusCode == 407) { + closeResponse(response); + LOG.error(message.toString()); + throw new PermissionDeniedException(message.toString()); + } + if (statusCode == 404 || statusCode == 410) { + closeResponse(response); + LOG.error(message.toString()); + throw new UnknownUidException(message.toString()); + } + if (statusCode == 408) { + closeResponse(response); + LOG.error(message.toString()); + throw new OperationTimeoutException(message.toString()); + } + if (statusCode == 412) { + closeResponse(response); + LOG.error(message.toString()); + throw new PreconditionFailedException(message.toString()); + } + if (statusCode == 418) { + closeResponse(response); + LOG.error(message.toString()); + throw new UnsupportedOperationException("Sorry, no cofee: " + message.toString()); + } + + closeResponse(response); + LOG.error(message.toString()); + throw new ConnectorException(message.toString()); + } + + void closeResponse(CloseableHttpResponse response) { + // to avoid pool waiting + if (response == null) + return; + try { + response.close(); + } catch (IOException e) { + LOG.warn(e, "Failed to close response: " + response); + } + } + + void closeClient(CloseableHttpClient client){ + if (client == null){ + return; + } + try { + client.close(); + } catch (IOException e) { + LOG.warn(e, "Failed to close client."); + } + } + + // filter json objects by substring: + JSONArray substringFiltering(JSONArray inputJsonArray, String attrName, String subValue) { + JSONArray jsonArrayOut = new JSONArray(); + // String attrName = attribute.getName().toString(); + // LOGGER.info("\n\tSubstring filtering: {0} ({1})", attrName, + // subValue); + for (int i = 0; i < inputJsonArray.length(); i++) { + JSONObject jsonObject = inputJsonArray.getJSONObject(i); + if (!jsonObject.has(attrName)) { + LOG.warn("\n\tProcessing JSON Object does not contain attribute {0}.", attrName); + return null; + } + if (jsonObject.has(attrName) && (jsonObject.get(attrName)).toString().contains(subValue)) { + // LOG.ok("value: {0}, subValue: {1} - MATCH: {2}", + // jsonObject.get(attrName).toString(), subValue, "YES"); + jsonArrayOut.put(jsonObject); + } + // else LOG.ok("value: {0}, subValue: {1} - MATCH: {2}", + // jsonObject.getString(attrName), subValue, "NO"); + } + return jsonArrayOut; + } + + JSONArray startsWithFiltering(Filter query, Attribute attr, OperationOptions options, String objClass) { + String attrValue = attr.getValue().get(0).toString(); + HttpGet request; + URIBuilder getUri = null; + if (attrValue != null) { + try { + getUri = getURIBuilder(); + getUri.setPath(URI_BASE_PATH + URI_USER_PATH + URI_SEARCH_PATH); + getUri.addParameter(PARAM_USERNAME, attrValue); + if (options.getPagedResultsOffset() != null || options.getPageSize() != null) { + int pageNumber = options.getPagedResultsOffset(); + int usersPerPage = options.getPageSize(); + int startAt = (pageNumber * usersPerPage) - usersPerPage; + //LOG.info("\n\tpage: {0}, users per page {1}, start at: {2}", pageNumber, usersPerPage, startAt); + getUri.addParameter(PARAM_MAX_RESULTS, String.valueOf(usersPerPage)); + getUri.addParameter(PARAM_START_AT, String.valueOf(startAt)); + } + ; + request = new HttpGet(getUri.build()); + + JSONArray objectsArray = new JSONArray(); + if (objClass.equals(ObjectClass.GROUP_NAME)) { + JSONObject object = callRequest(request, true, CONTENT_TYPE_JSON); + objectsArray = object.getJSONArray(ATTR_GROUPS); + } + if (objClass.equals(ObjectClass.ACCOUNT_NAME) || objClass.equals(PROJECT_NAME)) { + objectsArray = callRequest(request); + } + // handleObjects(objectsArray, handler, options, objClass); + return objectsArray; + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Get operation failed: problem occurred during executing URI: ").append(getUri) + .append(", using attribute: ").append(attrValue).append("\n\t").append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + } else + throwNullAttrException(query); + return null; + } + + // method called when attribute of query filter is null: + void throwNullAttrException(Filter query) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg + .append("Get operation failed: problem occurred because of not provided attribute of query filter: ") + .append(query); + LOG.error(exceptionMsg.toString()); + throw new InvalidAttributeValueException(exceptionMsg.toString()); + } + + // create uri from base host: + URIBuilder getURIBuilder() { + String baseHost = configuration.getBaseUrl(); + URIBuilder uri = new URIBuilder(); + uri.setScheme("https"); + uri.setHost(baseHost); + uri.setPath(URI_BASE_PATH); + return uri; + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // avatar processing: + void createOrUpdateAvatar(byte[] avatar, String uid, String objectName) { + // crop image to square and resize it to needed size: + byte[] resizedImage = resizeAndCropImage(avatar, 48, 48); + String username = null, key = null; + + CloseableHttpClient client = HttpClientBuilder.create().build(); + + boolean needsCropping = false; + int cropperWidth, cropperOffsetX, cropperOffsetY; + JSONObject cropBody = new JSONObject(); + URIBuilder uri = getURIBuilder(); + JSONObject responseObject = new JSONObject(); + String avatarId = null; + + // delete old avatar before the new one will be successfully set: + deleteAvatar(uid, objectName); + + // uri for uploading user avatar: + if (objectName.equals(USER_NAME)) { // USER + username = getUsernameFromUid(uid); + uri.setPath(URI_BASE_PATH + URI_USER_PATH + URI_TEMP_AVATAR_PATH); + uri.addParameter(PARAM_USERNAME, username); + uri.addParameter(PARAM_FILENAME, username + ".jpeg"); + } + + // uri for uploading project avatar + if (objectName.equals(PROJECT_NAME)) { // PROJECT + key = getProjectKeyFromUid(uid); + uri.setPath(URI_BASE_PATH + URI_PROJECT_PATH + "/" + key + URI_TEMP_AVATAR_PATH); + uri.addParameter(PARAM_FILENAME, key + ".jpeg"); + // LOGGER.info("\n\tupload project avatar: {0}", uri.toString()); + } + + HttpEntityEnclosingRequestBase postRequest; + HttpEntityEnclosingRequestBase putRequest; + CloseableHttpResponse response = null; + // 1st step: upload avatar: + try { + postRequest = new HttpPost(uri.build()); + if (resizedImage != null) { + postRequest.setHeader("X-Atlassian-Token", "no-check"); + postRequest.setHeader("Authorization", "Basic YWRtaW5pc3RyYXRvcjp0cmFpbmluZw=="); + postRequest.setHeader("Content-Type", CONTENT_TYPE_JPEG_IMAGE); + postRequest.setEntity(new ByteArrayEntity(resizedImage)); + + response = (CloseableHttpResponse) client.execute(postRequest); + // print response code: + //LOG.ok("response code: {0}", String.valueOf(response.getStatusLine().getStatusCode())); + // client.close(); + // DO NOT CLOSE response HERE !!! + processResponseErrors(response); + String result = EntityUtils.toString(response.getEntity()); + + closeResponse(response); + responseObject = new JSONObject(result); + + if (responseObject.has("needsCropping")) { + needsCropping = "true".equals(responseObject.get("needsCropping").toString()); + // LOGGER.info("\n\tneedsCropping-{0}", needsCropping); + } + if (responseObject.has("cropperWidth")) { + cropperWidth = (int) responseObject.get("cropperWidth"); + // LOGGER.info("\n\tcropperWidth-{0}", cropperWidth); + cropBody.put("cropperWidth", cropperWidth); + } + if (responseObject.has("cropperOffsetX")) { + cropperOffsetX = (int) responseObject.get("cropperOffsetX"); + // LOGGER.info("\n\tcropperOffsetX-{0}",cropperOffsetX); + cropBody.put("cropperOffsetX", cropperOffsetX); + } + if (responseObject.has("cropperOffsetY")) { + cropperOffsetY = (int) responseObject.get("cropperOffsetY"); + // LOGGER.info("\n\tcropperOffsetY-{0}", cropperOffsetY); + cropBody.put("cropperOffsetY", cropperOffsetY); + } + if (responseObject.has("id")) { + avatarId = responseObject.getString("id"); + // LOGGER.info("\n\tid-{0}", cropperOffsetY); + } + } + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Uploading of the avatar failed: problem occurred during building URI: ").append(uri) + .append("\n\t").append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Uploading of the avatar failed: problem occured during request execution: \n\t") + .append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString()); + } + // 2nd step: crop avatar: + if (needsCropping) { + uri.removeQuery(); + if (objectName.equals(USER_NAME)) { // USER + uri.setPath(URI_BASE_PATH + URI_USER_PATH + URI_AVATAR_PATH); + uri.addParameter(PARAM_USERNAME, username); + } + if (objectName.equals(PROJECT_NAME)) { // PROJECT + uri.setPath(URI_BASE_PATH + URI_PROJECT_PATH + "/" + key + URI_AVATAR_PATH); + LOG.info("\n\tcrop project avatar: {0}", uri.toString()); + } + try { + postRequest = new HttpPost(uri.build()); + postRequest.setHeader("X-Atlassian-Token", "no-check"); + postRequest.setHeader("Content-Type", CONTENT_TYPE_JSON); + + HttpEntity entity = new ByteArrayEntity(cropBody.toString().getBytes("UTF-8")); + postRequest.setEntity(entity); + + response = (CloseableHttpResponse) client.execute(postRequest); + // print response code: + LOG.ok("response code: {0}", String.valueOf(response.getStatusLine().getStatusCode())); + // client.close(); + // DO NOT CLOSE response HERE !!! + processResponseErrors(response); + String result = EntityUtils.toString(response.getEntity()); + + closeResponse(response); + LOG.ok("response body: {0}", result); + responseObject = new JSONObject(result); + + avatarId = responseObject.getString("id"); + // LOGGER.info("\n\t2nd step: {0}", responseObject.toString()); + + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Cropping of the avatar failed: problem occurred during building URI: ").append(uri) + .append("\n\t").append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } catch (UnsupportedEncodingException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg + .append("Cropping of the avatar failed: problem occurred during encoding request body for URI: ") + .append(uri).append("\n\t").append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Cropping of the avatar failed: problem occured during request execution: \n\t") + .append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString()); + } + + } + // 3rd step: confirm avatar: + + try { + uri.removeQuery(); + if (objectName.equals(USER_NAME)) { // USER + uri.setPath(URI_BASE_PATH + URI_USER_PATH + URI_AVATAR_PATH); + uri.addParameter(PARAM_USERNAME, username); + } + if (objectName.equals(PROJECT_NAME)) { // PROJECT + uri.setPath(URI_BASE_PATH + URI_PROJECT_PATH + "/" + key + URI_AVATAR_PATH); + LOG.info("\n\tconfirm project avatar: {0}", uri.toString()); + } + + putRequest = new HttpPut(uri.build()); + putRequest.setHeader("X-Atlassian-Token", "no-check"); + putRequest.setHeader("Content-Type", CONTENT_TYPE_JSON); + JSONObject confirmBody = new JSONObject(); + confirmBody.put("id", avatarId); + + HttpEntity entity = new ByteArrayEntity(confirmBody.toString().getBytes("UTF-8")); + putRequest.setEntity(entity); + + response = (CloseableHttpResponse) client.execute(putRequest); + + LOG.ok("response code: {0}", String.valueOf(response.getStatusLine().getStatusCode())); + // client.close(); + // DO NOT CLOSE response HERE !!! + processResponseErrors(response); + + closeResponse(response); + + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Confirmation of the avatar failed: problem occurred during building URI: ").append(uri) + .append("\n\t").append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } catch (UnsupportedEncodingException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg + .append("Confirmation of the avatar failed: problem occurred during encoding request body for URI: ") + .append(uri).append("\n\t").append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Confirmation of the avatar failed: problem occured during request execution: \n\t") + .append(e.getLocalizedMessage()); + closeResponse(response); + closeClient(client); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString()); + } + } + + void deleteAvatar(String uid, String objectName) { + // user: + // http://example.com:8080/jira/rest/api/2/user/avatar/?username= + // project: + // http://example.com:8080/jira/rest/api/2/project//avatar/ + URIBuilder deleteUri = getURIBuilder(); + CloseableHttpResponse response = null; + if (uid != null) { + try { + deleteUri = getURIBuilder(); + + String avatarId = getAvatarId(uid, objectName); + if (avatarId == null) { + LOG.warn("Deleting of the avatar ignored: Requested avatar is probably default system avatar."); + return; + } + + if (objectName.equals(PROJECT_NAME)) { + String key = getProjectKeyFromUid(uid); + deleteUri.setPath(URI_BASE_PATH + URI_PROJECT_PATH + "/" + key + URI_AVATAR_PATH + "/" + avatarId); + } + if (objectName.equals(USER_NAME)) { + String username = getUsernameFromUid(uid); + deleteUri.setPath(URI_BASE_PATH + URI_USER_PATH + URI_AVATAR_PATH + "/" + avatarId); + deleteUri.addParameter(PARAM_USERNAME, username); + } + + HttpDelete request = new HttpDelete(deleteUri.build()); + request.addHeader("Authorization", "Basic " + authEncoding()); + response = execute(request); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 401) { + LOG.warn("Deleting of the avatar was ignored: Avatar with id {0} is probably system avatar.", + avatarId); + return; + } else { + processResponseErrors(response); + } + closeResponse(response); + + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Deletion of the avatar failed: problem occurred during executing URI: ") + .append(deleteUri).append(", using uid attribute: ").append(uid).append("\n\t") + .append(e.getLocalizedMessage()); + closeResponse(response); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + } + } + + String getAvatarId(String uid, String objectName) { + URIBuilder getUri = getURIBuilder(); + if (uid != null) { + try { + getUri = getURIBuilder(); + JSONObject object = null; + + if (objectName.equals(PROJECT_NAME)) { + getUri.setPath(URI_BASE_PATH + URI_PROJECT_PATH + "/" + uid); + } + if (objectName.equals(USER_NAME)) { + getUri.setPath(URI_BASE_PATH + URI_USER_PATH); + getUri.addParameter(PARAM_KEY, uid); + } + + HttpGet request = new HttpGet(getUri.build()); + object = callRequest(request, true, CONTENT_TYPE_JSON); + // LOGGER.info("project key: {0}", object.getString(ATTR_KEY)); + JSONObject avatarUrls = object.getJSONObject(ATTR_AVATAR_URLS); + String avatarUrl = avatarUrls.getString("48x48"); + URIBuilder avatarUri = new URIBuilder(avatarUrl); + List queryParameters = avatarUri.getQueryParams(); + + for (NameValuePair pair : queryParameters) { + String name = pair.getName(); + if (name.equals("avatarId")) { + return pair.getValue(); + } + } + + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Getting avatar uid failed: problem occurred during executing URI: ").append(getUri) + .append("\n\t").append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + } + return null; + } + + byte[] resizeAndCropImage(byte[] image, int width, int height) { + ByteArrayInputStream bis = new ByteArrayInputStream(image); + BufferedImage buffImage; + try { + buffImage = ImageIO.read(bis); + if (buffImage == null) { + String exceptionMsg = "\n\tBuffering of the avatar for resize failed!"; + LOG.error(exceptionMsg); + throw new ConnectorIOException(exceptionMsg); + } + // crop image if needed: + int originalWidth = buffImage.getWidth(); + int originalHeight = buffImage.getHeight(); + BufferedImage croppedBuffImage; + if (originalWidth > originalHeight) { + croppedBuffImage = buffImage.getSubimage((originalWidth / 2 - originalHeight / 2), 0, originalHeight, + originalHeight); + } else if (originalHeight > originalWidth) { + croppedBuffImage = buffImage.getSubimage(0, (originalHeight / 2 - originalWidth / 2), originalWidth, + originalWidth); + } else { + croppedBuffImage = buffImage; + } + + // LOGGER.info("\n\tConverting image format and size..."); + // resize image: + BufferedImage resizedBuffImage = new BufferedImage(width, height, 5); + Graphics2D imgGraphics = resizedBuffImage.createGraphics(); + imgGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + imgGraphics.drawImage(croppedBuffImage, 0, 0, width, height, null); + imgGraphics.dispose(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + // LOGGER.info("\n\tWritting output image..."); + if (ImageIO.write(resizedBuffImage, "jpeg", bos) == false) { + LOG.error("\n\tConverting image format and size faild."); + return null; + } else { + // LOGGER.info("\n\tConverting finished successfully."); + byte[] resizedImage = bos.toByteArray(); + // ImageIO.write(resizedImage, "png", new File("D:\\out.png")); + return resizedImage; + + } + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg + .append("Converting avatar image format failed: problem occured during converting format and writing it to byte array stream: \n\t") + .append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString()); + } + } + + byte[] getAvatar(String avatarUrl, ObjectClass objClass) { + byte[] result = null; + try { + URIBuilder getUri = new URIBuilder(avatarUrl); + HttpGet request = new HttpGet(getUri.build()); + request.addHeader("Authorization", "Basic " + authEncoding()); + request.addHeader("User-Agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.65 Safari/537.31"); + + CloseableHttpResponse response = execute(request); + processResponseErrors(response); + + HttpEntity entity = response.getEntity(); + result = EntityUtils.toByteArray(entity); + String imageType = URLConnection.guessContentTypeFromStream(new ByteArrayInputStream(result)); + // LOGGER.info("\n\ttype: {0}", contentType); + + // SVG avatar: + if (imageType.contains("xml")) { + // LOGGER.info("\n\tSVG avatar"); + TranscoderInput inputSVGimage = new TranscoderInput(avatarUrl); + OutputStream outputJPEGstream = new ByteArrayOutputStream(); + TranscoderOutput outputJPEGimage = new TranscoderOutput(outputJPEGstream); + JPEGTranscoder SVGtoJPEGconverter = new JPEGTranscoder(); + SVGtoJPEGconverter.addTranscodingHint(JPEGTranscoder.KEY_QUALITY, new Float(1)); + try { + SVGtoJPEGconverter.transcode(inputSVGimage, outputJPEGimage); + result = ((ByteArrayOutputStream) outputJPEGstream).toByteArray(); + // ByteArrayInputStream bis = new + // ByteArrayInputStream(result); + // BufferedImage img = ImageIO.read(bis); + // ImageIO.write(img, "jpeg", new File("D:\\outJPEG.jpeg")); + outputJPEGstream.flush(); + outputJPEGstream.close(); + closeResponse(response); + return result; + } catch (TranscoderException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Converting from SVG format to JPEG foramt failed").append("\n\t") + .append(e.getLocalizedMessage()); + closeResponse(response); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + } else { + return result; + } + } catch (IOException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Getting avatar failed: problem occured during determining of image format:") + .append("\n\t").append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorIOException(exceptionMsg.toString()); + } catch (URISyntaxException e1) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Getting avatar failed: Problem occured during bilding URI: ").append(avatarUrl) + .append("\n\t").append(e1.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + // return result; + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + String getUsernameFromUid(String uid) { + URIBuilder getUri = getURIBuilder(); + if (uid != null) { + try { + getUri = getURIBuilder(); + getUri.setPath(URI_BASE_PATH + URI_USER_PATH); + getUri.addParameter(UID, uid); + HttpGet request = new HttpGet(getUri.build()); + JSONObject user = callRequest(request, true, CONTENT_TYPE_JSON); + // LOGGER.info("username: {0}", user.getString(ATTR_NAME)); + return user.getString(ATTR_NAME); + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Get username form user ID failed: problem occurred during executing URI: ") + .append(getUri).append(", using uid attribute: ").append(uid).append("\n\t") + .append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + } + return null; + } + + String getUserIdFromName(String username) { + URIBuilder getUri = getURIBuilder(); + if (username != null) { + try { + getUri = getURIBuilder(); + getUri.setPath(URI_BASE_PATH + URI_USER_PATH); + getUri.addParameter(PARAM_USERNAME, username); + HttpGet request = new HttpGet(getUri.build()); + JSONObject user = callRequest(request, true, CONTENT_TYPE_JSON); + LOG.info("username: {0}", user.getString(ATTR_KEY)); + return user.getString(ATTR_KEY); + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Get user ID form user username failed: problem occurred during executing URI: ") + .append(getUri).append("\n\t").append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + } + return null; + } + + String getProjectKeyFromUid(String uid) { + URIBuilder getUri = getURIBuilder(); + if (uid != null) { + try { + getUri = getURIBuilder(); + getUri.setPath(URI_BASE_PATH + URI_PROJECT_PATH + "/" + uid); + HttpGet request = new HttpGet(getUri.build()); + JSONObject project = callRequest(request, true, CONTENT_TYPE_JSON); + LOG.info("project key: {0}", project.getString(ATTR_KEY)); + return project.getString(ATTR_KEY); + } catch (URISyntaxException e) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Get project key form uid failed: problem occurred during executing URI: ") + .append(getUri).append("\n\t").append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + throw new ConnectorException(exceptionMsg.toString()); + } + } + return null; + } + + T addAttr(ConnectorObjectBuilder builder, String attrName, T attrVal) { + if (attrVal != null) { + builder.addAttribute(attrName, attrVal); + } + return attrVal; + } + + String getStringAttr(Set attributes, String attrName) throws InvalidAttributeValueException { + return getAttr(attributes, attrName, String.class); + } + + GuardedString getGuardedStringAttr(Set attributes, String attrName) + throws InvalidAttributeValueException { + return getAttr(attributes, attrName, GuardedString.class); + } + + T getAttr(Set attributes, String attrName, Class type) + throws InvalidAttributeValueException { + return getAttr(attributes, attrName, type, null); + } + + byte[] getByteArrayAttr(Set attributes, String attrName) + throws InvalidAttributeValueException { + return getAttr(attributes, attrName, byte[].class); + } + + @SuppressWarnings("unchecked") + private T getAttr(Set attributes, String attrName, Class type, T defaultVal) + throws InvalidAttributeValueException { + for (Attribute attr : attributes) { + if (attrName.equals(attr.getName())) { + List vals = attr.getValue(); + if (vals == null || vals.isEmpty()) { + // set empty value + return null; + } + if (vals.size() == 1) { + Object val = vals.get(0); + if (val == null) { + // set empty value + return null; + } + if (type.isAssignableFrom(val.getClass())) { + return (T) val; + } + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Unsupported type ").append(val.getClass()).append(" for attribute ") + .append(attrName).append(", value: ").append(val); + LOG.error(exceptionMsg.toString()); + throw new InvalidAttributeValueException(exceptionMsg.toString()); + } + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("More than one value for attribute ").append(attrName).append(", values: ") + .append(vals); + LOG.error(exceptionMsg.toString()); + throw new InvalidAttributeValueException(exceptionMsg.toString()); + } + } + // set default value when attrName not in changed attributes + return defaultVal; + } + + void getIfExists(JSONObject jsonObj, String attr, ConnectorObjectBuilder builder, boolean isMultiValue) { + if (jsonObj.has(attr) && jsonObj.get(attr) != null && !JSONObject.NULL.equals(jsonObj.get(attr))) { + if (isMultiValue) { + JSONArray attrJSONArray = jsonObj.getJSONArray(attr); + if (attrJSONArray != null) { + int size = attrJSONArray.length(); + ArrayList attrStringArray = new ArrayList(); + for (int i = 0; i < size; i++) { + attrStringArray.add(attrJSONArray.get(i).toString()); + } + builder.addAttribute(attr, attrStringArray.toArray()); + } + } else + addAttr(builder, attr, jsonObj.get(attr)); + } + } + + Uid getUidIfExists(JSONObject jsonObj, String attr, ConnectorObjectBuilder builder) { + if (jsonObj.has(attr) && jsonObj.get(attr) != null && !JSONObject.NULL.equals(jsonObj.get(attr))) { + Uid uid = new Uid(jsonObj.getString(attr)); + builder.setUid(uid); + return uid; + } else { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Primary identifier '").append(attr).append("' is missing."); + LOG.error(exceptionMsg.toString()); + throw new ConfigurationException(exceptionMsg.toString()); + } + } + + void getNameIfExists(JSONObject jsonObj, String attr, ConnectorObjectBuilder builder) { + if (jsonObj.has(attr) && jsonObj.get(attr) != null && !JSONObject.NULL.equals(jsonObj.get(attr))) { + builder.setName(new Name(jsonObj.getString(attr))); + } else { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append("Secondary identifier '").append(attr).append("' is missing."); + LOG.error(exceptionMsg.toString()); + throw new ConfigurationException(exceptionMsg.toString()); + } + } + + // processing of multivalue attribute containing JSONArray attribute + // (applicable for e.g. extended attributes of users/groups/projects) + // get string array of attributes 'name' from multivalue attribute 'item': + ArrayList getMultiAttrItems(JSONObject object, String attrName, String subAttrName) { + + JSONObject itemJSONObject = null; + JSONArray itemsJSONArray = (JSONArray) object.get(attrName); + int size = itemsJSONArray.length(); + if (size > 0) { + ArrayList itemsArray = new ArrayList(); + // String[] itemsArray = new String[size]; + // JSONArray itemsJSONArray = roles.keySet().toArray(new + // String[roles.keySet().size()]); + // LOG.info("\n\tItems Count: {0} of atribute {1}: ' {2} '", + // size, attrName, itemsJSONArray); + + for (int i = 0; i < itemsJSONArray.length(); i++) { + // LOG.ok("\n\tProcessing {0}/{1} {2}: {3}", i + 1, + // itemsJSONArray.length(), attrName, + // itemsJSONArray.getJSONObject(i).get(subAttrName)); + + itemJSONObject = itemsJSONArray.getJSONObject(i); + itemsArray.add(itemJSONObject.getString(subAttrName)); + // LOG.info("\n\tGroup {0}: {1}", i+1, itemsArray[i]); + } + return itemsArray; + } + + return null; + } + + void putFieldIfExists(Set attributes, String fieldName, JSONObject jo) { + String fieldValue = getStringAttr(attributes, fieldName); + if (fieldValue != null) { + jo.put(fieldName, fieldValue); + } + } + + void test() { + URIBuilder uriBuilder = getURIBuilder() + .setPath(URI_BASE_PATH + PATH_STEMS) + .addParameter("wsLiteObjectType", "WsRestFindStemsLiteRequest") + .addParameter("stemName", configuration.getRootStem()) + .addParameter("stemQueryFilterType", "FIND_BY_STEM_NAME"); + try { + HttpGet request = new HttpGet(uriBuilder.build()); + JSONObject response = callRequest(request, true, CONTENT_TYPE_JSON); + System.out.println("Got response: " + response); + + checkSuccess(response, WS_FIND_STEMS_RESULTS); + JSONArray stemResults = getArray(response, WS_FIND_STEMS_RESULTS, STEM_RESULTS); + int stemsFound = stemResults.length(); + if (stemsFound == 0) { + throw new IllegalStateException("No stems named '" + configuration.getRootStem() + "' were found"); + } else if (stemsFound != 1) { + throw new IllegalStateException("More than one stem '" + configuration.getRootStem() + "' found: " + stemsFound); + } + } catch (RuntimeException|URISyntaxException e) { + throw processException(e, uriBuilder, "Test"); + } + } + + public void checkSuccess(JSONObject response, String rootName) { + Object success = get(response, rootName, RESULT_METADATA, SUCCESS); + if (!"T".equals(success)) { + throw new IllegalStateException("Request was not successful: " + success); + } + } + + public Object get(JSONObject object, String... items) { + if (items.length == 0) { + throw new IllegalArgumentException("Empty item path"); + } + for (int i = 0; i < items.length - 1; i++) { + if (!object.has(items[i])) { + throw new IllegalStateException("Item " + Arrays.asList(items).subList(0, i) + " was not found"); + } + Object o = object.get(items[i]); + if (o instanceof JSONArray) { + JSONArray array = (JSONArray) o; + if (array.length() == 0) { + throw new IllegalStateException("Item " + Arrays.asList(items).subList(0, i) + " is an empty array"); + } 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 (o instanceof JSONObject) { + object = (JSONObject) o; + } else { + throw new IllegalStateException("Item " + Arrays.asList(items).subList(0, i) + " is neither object nor array; it is " + o.getClass()); + } + } + return object.get(items[items.length - 1]); + } + + public JSONArray getArray(JSONObject object, String... items) { + Object rv = get(object, items); + if (rv instanceof JSONArray) { + return (JSONArray) rv; + } else { + throw new IllegalStateException("Item " + Arrays.asList(items) + " should be an array but it's " + rv.getClass()); + } + } + + public ConnectorException processException(Exception e, URIBuilder uriBuilder, final String operationName) { + StringBuilder exceptionMsg = new StringBuilder(); + exceptionMsg.append(operationName).append(" failed: problem occurred during executing URI: ").append(uriBuilder) + .append("\n\t").append(e.getLocalizedMessage()); + LOG.error(exceptionMsg.toString()); + return new ConnectorException(exceptionMsg.toString(), e); + } + + public boolean isSuccess(JSONObject object) { + return "T".equals(getStringOrNull(object, SUCCESS)); + } + + public String getStringOrNull(JSONObject object, String item) { + if (object.has(item)) { + return getString(object, item); + } else { + return null; + } + } + + public String getString(JSONObject object, String item) { + return (String) get(object, item); // todo error handling + } +} 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 new file mode 100644 index 0000000..1fe1993 --- /dev/null +++ b/src/main/resources/com/evolveum/polygon/connector/grouper/rest/Messages.properties @@ -0,0 +1,8 @@ +username.display=Username +username.help=Please provide the administrator user name for the jira enabled service you are logging into + +password.display=Password +password.help=Please provide the administrator password used to connect to the jira enabled service + +baseUrl.display=Base URL +baseUrl.help=Please provide the base url for access to resources (e.g. host:port/rest/api/2/user) \ No newline at end of file diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/AccountTests.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/AccountTests.java new file mode 100644 index 0000000..a5e190a --- /dev/null +++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/AccountTests.java @@ -0,0 +1,79 @@ +package com.evolveum.polygon.connector.grouper.test; +/******************************************************************************* + * Copyright 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. + ******************************************************************************/ + +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 static org.testng.AssertJUnit.assertEquals; + +/** + * @author surmanek + * @author mederly + * + */ +public class AccountTests extends GrouperTestHelper { + + @Test(priority = 1) + public void initTest() { + grouperConnector.init(getConfiguration()); + cleanUp(); + } + + @Test(priority = 3) + public void schemaTest() { + grouperConnector.schema(); + } + + @Test(priority = 3) + public void testTest() { + grouperConnector.test(); + } + + @Test(priority = 4) + public void nameEqualsFilteringForAccountsTest() { + // filtering: + results.clear(); + AttributeFilter filter = (EqualsFilter) FilterBuilder + .equalTo(AttributeBuilder.build(Name.NAME, "banderson")); + + grouperConnector.executeQuery(accountObjectClass, filter, handler, options); + assertEquals("Wrong # of users retrieved", results.size(), 1); + ConnectorObject user = results.get(0); + System.out.println("Found user: " + user); + } + + @Test(priority = 6) + public void listingAccountsTest() { + results.clear(); + grouperConnector.executeQuery(accountObjectClass, null, handler, options); + + assertEquals("Wrong # of users retrieved", results.size(), 1); + ConnectorObject user = results.get(0); + System.out.println("Found user: " + user); + } + + + @Test(priority = 20) + public void disposeTest() { + grouperConnector.dispose(); + } + + private void cleanUp() { + results.clear(); + } +} diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTests.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTests.java new file mode 100644 index 0000000..c847e92 --- /dev/null +++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/GroupTests.java @@ -0,0 +1,94 @@ +package com.evolveum.polygon.connector.grouper.test; +/******************************************************************************* + * Copyright 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. + ******************************************************************************/ + +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 static org.testng.AssertJUnit.assertEquals; + +/** + * @author surmanek + * @author mederly + * + */ +public class GroupTests extends GrouperTestHelper { + + private String uuid; + + @Test(priority = 1) + public void initTest() { + grouperConnector.init(getConfiguration()); + cleanUp(); + } + + @Test(priority = 2) + public void schemaTest() { + grouperConnector.schema(); + } + + @Test(priority = 3) + public void testTest() { + grouperConnector.test(); + } + + @Test(priority = 4) + public void findByGroupName() { + // filtering: + results.clear(); + AttributeFilter filter = (EqualsFilter) FilterBuilder + .equalTo(AttributeBuilder.build(Name.NAME, "etc:sysadmingroup")); + + grouperConnector.executeQuery(ObjectClass.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 = 5) + public void findByGroupUuid() { + // filtering: + results.clear(); + AttributeFilter filter = (EqualsFilter) FilterBuilder + .equalTo(AttributeBuilder.build(Uid.NAME, uuid)); + + grouperConnector.executeQuery(ObjectClass.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 = 6) + public void allGroupsTest() { + results.clear(); + grouperConnector.executeQuery(ObjectClass.GROUP, null, handler, options); + for (ConnectorObject group : results) { + System.out.println("Found group: " + group); + } + } + + + @Test(priority = 20) + public void disposeTest() { + grouperConnector.dispose(); + } + + private void cleanUp() { + results.clear(); + } +} diff --git a/src/test/java/com/evolveum/polygon/connector/grouper/test/GrouperTestHelper.java b/src/test/java/com/evolveum/polygon/connector/grouper/test/GrouperTestHelper.java new file mode 100644 index 0000000..f9e6482 --- /dev/null +++ b/src/test/java/com/evolveum/polygon/connector/grouper/test/GrouperTestHelper.java @@ -0,0 +1,102 @@ +package com.evolveum.polygon.connector.grouper.test; +/******************************************************************************* + * Copyright 2017 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. + ******************************************************************************/ + +import com.evolveum.polygon.connector.grouper.rest.GrouperConfiguration; +import com.evolveum.polygon.connector.grouper.rest.GrouperConnector; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.common.security.GuardedString; +import org.identityconnectors.framework.common.objects.ConnectorObject; +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.SearchResult; +import org.identityconnectors.framework.spi.SearchResultsHandler; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * @author surmanek + * @author mederly + */ +public class GrouperTestHelper { + + private static final String BASE_URL = "192.168.56.101:9443"; + private static final String ADMIN_USERNAME = "banderson"; + private static final String ADMIN_PASSWORD = "password"; + private static final String SUPER_GROUP = "etc:sysadmingroup"; + private static final String ROOT_STEM = "etc"; + private static final String SUBJECT_SOURCE = "ldap"; + + protected final GrouperConnector grouperConnector = new GrouperConnector(); + protected final OperationOptions options = new OperationOptions(new HashMap()); + protected static final Log LOG = Log.getLog(GrouperConnector.class); + protected final ObjectClass accountObjectClass = ObjectClass.ACCOUNT; + + protected GuardedString password = new GuardedString("secret".toCharArray()); + protected final ArrayList results = new ArrayList<>(); + protected SearchResultsHandler handler = new SearchResultsHandler() { + @Override + public boolean handle(ConnectorObject connectorObject) { + results.add(connectorObject); + return true; + } + + @Override + public void handleResult(SearchResult result) { + } + }; + + //group variables: + protected static final ObjectClass groupObjectClass = ObjectClass.GROUP; + protected final ArrayList groupResults = new ArrayList<>(); + protected SearchResultsHandler groupHandler = new SearchResultsHandler() { + @Override + public boolean handle(ConnectorObject connectorObject) { + groupResults.add(connectorObject); + return true; + } + + @Override + public void handleResult(SearchResult result) { + } + }; + + //project variables: + protected static final ObjectClass projectObjectClass = new ObjectClass("PROJECT"); + protected final ArrayList projectResults = new ArrayList<>(); + protected SearchResultsHandler projectHandler = new SearchResultsHandler() { + @Override + public boolean handle(ConnectorObject connectorObject) { + projectResults.add(connectorObject); + return true; + } + + @Override + public void handleResult(SearchResult result) { + } + }; + + protected GrouperConfiguration getConfiguration() { + GrouperConfiguration config = new GrouperConfiguration(); + config.setBaseUrl(BASE_URL); + config.setUsername(ADMIN_USERNAME); + config.setPassword(new GuardedString(ADMIN_PASSWORD.toCharArray())); + config.setSuperGroup(SUPER_GROUP); + config.setRootStem(ROOT_STEM); + config.setIgnoreSslValidation(true); + config.setSubjectSource(SUBJECT_SOURCE); + return config; + } +}