From c3618feacad704d98c5ea8bf7f0394c80b452ddb Mon Sep 17 00:00:00 2001 From: Ethan Kromhout Date: Thu, 23 Aug 2018 14:48:57 -0400 Subject: [PATCH] Initial commit of Grouper To midPoint Docker Container --- grouper-to-midpoint-docker/Dockerfile | 13 + grouper-to-midpoint-docker/README | 43 ++ .../connection.properties | 7 + grouper-to-midpoint-docker/pom.xml | 93 ++++ .../resources.properties | 2 + grouper-to-midpoint-docker/roles.properties | 3 + .../groupermidpoint/GrouperToMidpoint.java | 517 ++++++++++++++++++ 7 files changed, 678 insertions(+) create mode 100644 grouper-to-midpoint-docker/Dockerfile create mode 100644 grouper-to-midpoint-docker/README create mode 100644 grouper-to-midpoint-docker/connection.properties create mode 100644 grouper-to-midpoint-docker/pom.xml create mode 100644 grouper-to-midpoint-docker/resources.properties create mode 100644 grouper-to-midpoint-docker/roles.properties create mode 100644 grouper-to-midpoint-docker/src/main/java/edu/unc/tier/groupermidpoint/GrouperToMidpoint.java diff --git a/grouper-to-midpoint-docker/Dockerfile b/grouper-to-midpoint-docker/Dockerfile new file mode 100644 index 0000000..f4c090f --- /dev/null +++ b/grouper-to-midpoint-docker/Dockerfile @@ -0,0 +1,13 @@ +FROM centos:latest +MAINTAINER ethan@unc.edu ekromhout@gmail.com +RUN yum -y install epel-release && yum -y update && yum -y install supervisor wget +# Installing Zulu Java +RUN rpm --import http://repos.azulsystems.com/RPM-GPG-KEY-azulsystems \ + && curl -o /etc/yum.repos.d/zulu.repo http://repos.azulsystems.com/rhel/zulu.repo \ + && yum -y install zulu-8 + +#RUN sed -i s/'nodaemon=false'/'nodaemon=true'/ /etc/supervisord.conf +COPY target/groupermidpoint-0.1-SNAPSHOT-jar-with-dependencies.jar /root/ +#CMD ["supervisord","-c","/etc/supervisord.conf"] +#/usr/java/latest/bin/java -cp rabbittrace-0.1-jar-with-dependencies.jar edu.unc.tier.rabbittrace.Trace '#' +CMD ["/usr/java/latest/bin/java","-cp","/root/groupermidpoint-0.1-SNAPSHOT-jar-with-dependencies.jar","edu.unc.tier.groupermidpoint.GrouperToMidpoint"] diff --git a/grouper-to-midpoint-docker/README b/grouper-to-midpoint-docker/README new file mode 100644 index 0000000..7841610 --- /dev/null +++ b/grouper-to-midpoint-docker/README @@ -0,0 +1,43 @@ +# Grouper to Midpoint Demonstration Container + +This is a class to demonstrate the usage of the +Unicon RabbitMQ plugin for Grouper, in conjuction +with midPoint's rest interface. This has a maven +project to build the Java class and a Dockerfile +to build a container to run it. + +The below several strings for configuration information +which can be edited in connection.properties +EXCHANGE_NAME +EXCHANGE_HOST +EXCHANGE_USER +EXCHANGE_SECRET +MIDPOINT_REST_URL +MIDPOINT_REST_USER +MIDPOINT_REST_SECRET + +There is also two property files that can be used to define +which groups membership additions and deletions should +result in adding or removing roles or resources in midPoint. + +roles.properties +resources.properties + +ref\:employee = employee + +Would add the midPoint employee role to a user when their subjectId was addedd to the ref:employee Grouper group. +It is necessary to escape the colons ":" generally present in Grouper group ids. + +This assumes the use of the Grouper Messaging system for RabbitMQ +and the following configuration items: + +changeLog.consumer.rabbitMqMessagingSample.publisher.messageQueueType = topic +changeLog.consumer.rabbitMqMessagingSample.publisher.queueOrTopicName = sampleTopic + +Other Grouper Messaging configuration items may need to be adjusted +to match your settings in connection.properties. + +Example Docker build and run syntax: + +docker build . +sudo docker run --network test-compose_back --name groupertomidpoint -d diff --git a/grouper-to-midpoint-docker/connection.properties b/grouper-to-midpoint-docker/connection.properties new file mode 100644 index 0000000..df17f09 --- /dev/null +++ b/grouper-to-midpoint-docker/connection.properties @@ -0,0 +1,7 @@ +EXCHANGE_NAME = sampleTopic +EXCHANGE_HOST = rabbitmq +EXCHANGE_USER = user +EXCHANGE_SECRET = guest +MIDPOINT_REST_URL = http://midpoint2.testbed.tier.internet2.edu:18080/ws/rest/ +MIDPOINT_REST_USER = administrator +MIDPOINT_REST_SECRET = 5ecr3t diff --git a/grouper-to-midpoint-docker/pom.xml b/grouper-to-midpoint-docker/pom.xml new file mode 100644 index 0000000..1dd807b --- /dev/null +++ b/grouper-to-midpoint-docker/pom.xml @@ -0,0 +1,93 @@ + + + + 4.0.0 + edu.unc.tier + groupermidpoint + 0.1-SNAPSHOT + jar + + Grouper To Midpoint + + + + + + maven-assembly-plugin + + + package + + single + + + + + + jar-with-dependencies + + + + + + + + + commons-codec + commons-codec + 1.9 + compile + + + commons-logging + commons-logging + 1.2 + + + + com.rabbitmq + amqp-client + 4.0.2 + + + + org.slf4j + slf4j-api + 1.7.12 + + + org.slf4j + slf4j-log4j12 + 1.7.25 + + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + + diff --git a/grouper-to-midpoint-docker/resources.properties b/grouper-to-midpoint-docker/resources.properties new file mode 100644 index 0000000..a4dbf7a --- /dev/null +++ b/grouper-to-midpoint-docker/resources.properties @@ -0,0 +1,2 @@ +app\:drupal_authorized = Drupal + diff --git a/grouper-to-midpoint-docker/roles.properties b/grouper-to-midpoint-docker/roles.properties new file mode 100644 index 0000000..1ff834e --- /dev/null +++ b/grouper-to-midpoint-docker/roles.properties @@ -0,0 +1,3 @@ +ref\:employee = employee +app\:canvas\:users = learner + diff --git a/grouper-to-midpoint-docker/src/main/java/edu/unc/tier/groupermidpoint/GrouperToMidpoint.java b/grouper-to-midpoint-docker/src/main/java/edu/unc/tier/groupermidpoint/GrouperToMidpoint.java new file mode 100644 index 0000000..86c762c --- /dev/null +++ b/grouper-to-midpoint-docker/src/main/java/edu/unc/tier/groupermidpoint/GrouperToMidpoint.java @@ -0,0 +1,517 @@ +package edu.unc.tier.groupermidpoint; + +import java.util.Set; +import java.util.Map; +import java.util.HashMap; +import com.rabbitmq.client.*; +import java.util.List; +import java.util.Iterator; +import java.io.IOException; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import java.util.ArrayList; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.apache.http.entity.StringEntity; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.auth.AuthenticationException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.w3c.dom.Node; +import org.w3c.dom.Element; +import java.io.StringReader; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.w3c.dom.bootstrap.DOMImplementationRegistry; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSSerializer; +import java.io.StringWriter; +import java.io.Writer; +import org.w3c.dom.ls.LSOutput; +import java.lang.InstantiationException; +import java.util.Properties; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + + + +public class GrouperToMidpoint { + + /* Exchange to which you have configured the grouper messages to route */ + + private static String EXCHANGE_NAME = ""; + private static String EXCHANGE_HOST = ""; + private static String EXCHANGE_USER = ""; + private static String EXCHANGE_SECRET = ""; + private static String EXCHANGE_ROUTING_KEY = ""; + private static String MIDPOINT_REST_URL = ""; + private static String MIDPOINT_REST_USER = ""; + private static String MIDPOINT_REST_SECRET = ""; + private static final boolean DEBUG = true; + + public static void main(String[] argv) throws Exception { + + Properties connectionProperties = new Properties(); + InputStream connectionPropertiesFile = null; + try { + connectionPropertiesFile = new FileInputStream("connection.properties"); + connectionProperties.load(connectionPropertiesFile); + EXCHANGE_NAME = (String) connectionProperties.get("EXCHANGE_NAME"); + EXCHANGE_HOST = (String) connectionProperties.get("EXCHANGE_HOST"); + EXCHANGE_USER = (String) connectionProperties.get("EXCHANGE_USER"); + EXCHANGE_SECRET = (String) connectionProperties.get("EXCHANGE_SECRET"); + EXCHANGE_ROUTING_KEY = (String) connectionProperties.get("EXCHANGE_ROUTING_KEY"); + MIDPOINT_REST_URL = (String) connectionProperties.get("MIDPOINT_REST_URL"); + MIDPOINT_REST_USER = (String) connectionProperties.get("MIDPOINT_REST_USER"); + MIDPOINT_REST_SECRET = (String) connectionProperties.get("MIDPOINT_REST_SECRET"); + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + if (connectionPropertiesFile != null) { + try { + connectionPropertiesFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + ConnectionFactory factory = new ConnectionFactory(); + factory.setHost(EXCHANGE_HOST); + factory.setUsername(EXCHANGE_USER); + factory.setPassword(EXCHANGE_SECRET); + Connection connection = factory.newConnection(); + Channel channel = connection.createChannel(); + boolean durable = true; + channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT, durable); + String queueName = channel.queueDeclare().getQueue(); + + /* Create hashmaps for the resources and roles you + want to sync over to midpoint. The keys here are + Grouper fully qualified group names. Those are also + the routing keys in the Unicon rabbitmq implementation. */ + + final HashMap roleGroups = new HashMap(); + final HashMap resourceGroups = new HashMap(); + //resourceGroups.put("stem1:group5","Test CSV: username"); + //resourceGroups.put("app:drupal_authorized","Drupal"); + //roleGroups.put("ref:employee","employee"); + //resourceGroups.put("app.drupal_authorized","Drupal"); + //roleGroups.put("ref.employee.All TIER Employees","employee"); + //roleGroups.put("app.canvas.users","learner"); + Properties rolesProperties = new Properties(); + InputStream rolesPropertiesFile = null; + try { + rolesPropertiesFile = new FileInputStream("roles.properties"); + rolesProperties.load(rolesPropertiesFile); + for (final String group: rolesProperties.stringPropertyNames()) { + roleGroups.put(group, rolesProperties.getProperty(group)); + } + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + if (rolesPropertiesFile != null) { + try { + rolesPropertiesFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + Properties resourcesProperties = new Properties(); + InputStream resourcesPropertiesFile = null; + try { + resourcesPropertiesFile = new FileInputStream("resources.properties"); + resourcesProperties.load(resourcesPropertiesFile); + for (final String resource: resourcesProperties.stringPropertyNames()) { + resourceGroups.put(resource, resourcesProperties.getProperty(resource)); + } + } catch (IOException ex) { + ex.printStackTrace(); + } finally { + if (resourcesPropertiesFile != null) { + try { + resourcesPropertiesFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + + /* TODO The above static strings and HashMaps should be fed from a property file. */ + + /* Bind to each of the desired group names / routing keys. */ + + if ( EXCHANGE_ROUTING_KEY != null && EXCHANGE_ROUTING_KEY.length() == 0 ) { + for ( Map.Entry entry : resourceGroups.entrySet()) { + String bindingKey = entry.getKey(); + channel.queueBind(queueName, EXCHANGE_NAME, bindingKey); + } + for ( Map.Entry entry : roleGroups.entrySet()) { + String bindingKey = entry.getKey(); + channel.queueBind(queueName, EXCHANGE_NAME, bindingKey); + } + } + else { + String bindingKey = EXCHANGE_ROUTING_KEY; + //Manually set to global queue bind + bindingKey = "#"; + channel.queueBind(queueName, EXCHANGE_NAME, bindingKey); + } + + /* The below implements a listener for messages by subsription + also known as the "Push API" so this class has to be running + when the messages you are interested in are published + or within the messages TTL. + */ + + if (DEBUG) { + System.out.println(" [*] Waiting for messages. To exit press CTRL+C"); + } + + Consumer consumer = new DefaultConsumer(channel) { + @Override + public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { + String message = new String(body, "UTF-8"); + if (DEBUG) { + System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'"); + } + String[] messageDetails = parseMessage(message); + String action = messageDetails[0]; + String group = messageDetails[1]; + //String group = groupWithColons.replace(":","."); + String subjectId = messageDetails[2]; + + /* The method getOidForName is just a helper method + so you don't have to put OIDs in your configuration + it was most needed for getting user OIDs by name + but is handy for resources and roles as well. + */ + + String subjectOid = getOidForName(subjectId,"users"); + //System.out.println("--" + subjectOid + "--"); + String type = ""; + String resourceOrRoleOid = ""; + String patchStatus = ""; + if ( resourceGroups.containsKey(group) ) { + type = "resources"; + resourceOrRoleOid = getOidForName(resourceGroups.get(group), type); + patchStatus = assignRoleOrResource ( subjectOid, resourceOrRoleOid, type, action); + } + if ( roleGroups.containsKey(group) ) { + type = "roles"; + resourceOrRoleOid = getOidForName(roleGroups.get(group), type); + patchStatus = assignRoleOrResource ( subjectOid, resourceOrRoleOid, type, action); + } + + if ( DEBUG ) { + System.out.println("Sent action " + action + " for subject " + subjectId + "(" + subjectOid + ")" + " to " + type + " OID " + resourceOrRoleOid + " because of group " + group + " with HTTP status result of " + patchStatus + "."); + } + + } + }; + channel.basicConsume(queueName, true, consumer); + } + private static String[] parseMessage(String message) { + + /* We are just intested in a few components of the payload + of the AMQP messages, so this extracts the event type + (MEMBERSHIP_ADD or MEMBERSHIP_DELETE) the fully qualified + group name, and the Grouper subject ID. + */ + + String eventType = ""; + String groupName = ""; + String subjectId = ""; + try { + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObject = (JSONObject) jsonParser.parse(message); + JSONArray esbEvent = (JSONArray) jsonObject.get("esbEvent"); + Iterator i = esbEvent.iterator(); + while (i.hasNext()) + { + JSONObject innerObj = (JSONObject) i.next(); + eventType = (String) innerObj.get("eventType"); + groupName = (String) innerObj.get("groupName"); + subjectId = (String) innerObj.get("subjectId"); + } + } + catch (Exception e) { + System.err.println(e.getMessage()); + } + return new String[] { eventType, groupName, subjectId }; + } + private static String getOidForName(String name, String type) { + + /* In Midpoint, most objects need to be referenced by OID in + the rest API. In order to avoid hard coding references to + specific OIDs in configuration, this function allows for + searching by name to retrieve an OID. This also avoids + having to change configuration in case you are changing + Midpoint instances, OIDs are generated and therefore + unique between installs. + */ + + String oid = ""; + try { + + /* Build a HTTP POST client, search operations in the Midpoint REST API are POSTS */ + + CloseableHttpClient client = HttpClients.createDefault(); + HttpPost httpPost = new HttpPost( MIDPOINT_REST_URL + type + "/search"); + httpPost.setHeader("Content-Type", "application/xml"); + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(MIDPOINT_REST_USER,MIDPOINT_REST_SECRET); + httpPost.addHeader(new BasicScheme().authenticate(creds, httpPost, null)); + + /* The Midpoint examples I found for the REST API were XML + so this builds a document based on the examples published + at https://github.com/Evolveum/midpoint/tree/master/samples/rest */ + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.newDocument(); + Element query = doc.createElement("q:query"); + query.setAttribute("xmlns:q", "http://prism.evolveum.com/xml/ns/public/query-3"); + query.setAttribute("xmlns:c", "http://midpoint.evolveum.com/xml/ns/public/common/common-3"); + doc.appendChild(query); + Element queryFilter = doc.createElement("q:filter"); + query.appendChild(queryFilter); + Element queryCompare = doc.createElement("q:equal"); + queryFilter.appendChild(queryCompare); + Element queryPath = doc.createElement("q:path"); + queryPath.insertBefore(doc.createTextNode("c:name"), queryPath.getLastChild()); + queryCompare.appendChild(queryPath); + Element queryValue = doc.createElement("q:value"); + queryValue.insertBefore(doc.createTextNode(name), queryValue.getLastChild()); + queryCompare.appendChild(queryValue); + final DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); + final DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS"); + final LSSerializer writer = impl.createLSSerializer(); + writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); + writer.getDomConfig().setParameter("xml-declaration", Boolean.TRUE); + + /* The below nonsense is to get a UTF-8 XML declaration, + the declaration defaulted to UTF-16 in my testing, + and sending a UTF-16 declaration to Midpoint threw + a 500 for me. This could just be a configuration issue. + */ + + LSOutput lsOutPut = impl.createLSOutput(); + lsOutPut.setEncoding("UTF-8"); + Writer stringWriter = new StringWriter(); + lsOutPut.setCharacterStream(stringWriter); + writer.write(doc,lsOutPut); + StringEntity stringEntity = new StringEntity(stringWriter.toString()); + httpPost.setEntity(stringEntity); + CloseableHttpResponse response = client.execute(httpPost); + BufferedReader rd = new BufferedReader( new InputStreamReader(response.getEntity().getContent())); + StringBuffer result = new StringBuffer(); + String line = ""; + while ((line = rd.readLine()) != null) { + result.append(line); + } + doc = builder.parse( new InputSource(new StringReader(result.toString()))); + doc.getDocumentElement().normalize(); + + /* Parse the document to get the OID, these had the form + + That example is from a Role search in my testing, + but were similar for Users and Resources. + */ + + NodeList nList = doc.getElementsByTagName("apti:object"); + for (int temp = 0; temp < nList.getLength(); temp++) { + Node nNode = nList.item(temp); + if (nNode.getNodeType() == Node.ELEMENT_NODE) { + Element eElement = (Element) nNode; + oid = eElement.getAttribute("oid"); + } + } + + } catch (ClassNotFoundException CNFE) { + System.err.println(CNFE.getMessage()); + CNFE.printStackTrace(); + + } catch (IllegalAccessException IAE) { + System.err.println(IAE.getMessage()); + IAE.printStackTrace(); + + } catch (InstantiationException IE) { + System.err.println(IE.getMessage()); + IE.printStackTrace(); + + } catch (IOException IOE) { + System.err.println(IOE.getMessage()); + IOE.printStackTrace(); + + } catch (SAXException SAXE) { + System.err.println(SAXE.getMessage()); + SAXE.printStackTrace(); + + } catch (AuthenticationException AE) { + System.err.println(AE.getMessage()); + AE.printStackTrace(); + + } catch (ParserConfigurationException PCE) { + System.err.println(PCE.getMessage()); + PCE.printStackTrace(); + + } catch (Exception e) { + /* Shouldn't get here... */ + e.printStackTrace(); + } + + return new String(oid); + } + private static String assignRoleOrResource(String subjectOid, String resourceOrRoleOid, String type, String action) { + + /* In Midpoint, roles and resources are added to user objects + so the user OID gets built into the rest URL, and then + the role or resource OID is included in the XML document + with the action to take. + */ + + String patchStatus = ""; + try { + + /* Build a HTTP PATCH client, update operations in the Midpoint REST API are PATCHES */ + + String midpointAction = ""; + if (action.equals("MEMBERSHIP_ADD")) { + midpointAction = "add"; + } else if (action.equals("MEMBERSHIP_DELETE")) { + midpointAction = "delete"; + } else { + return "Error, not defined action for " + action + "aborting patch."; + } + CloseableHttpClient client = HttpClients.createDefault(); + HttpPatch httpPatch = new HttpPatch( MIDPOINT_REST_URL + "/users/" + subjectOid); + httpPatch.setHeader("Content-Type", "application/xml"); + UsernamePasswordCredentials creds = new UsernamePasswordCredentials(MIDPOINT_REST_USER,MIDPOINT_REST_SECRET); + httpPatch.addHeader(new BasicScheme().authenticate(creds, httpPatch, null)); + + /* The Midpoint examples I found for the REST API were XML + so this builds a document based on the examples published + at https://github.com/Evolveum/midpoint/tree/master/samples/rest */ + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.newDocument(); + Element objectModification = doc.createElement("objectModification"); + doc.appendChild(objectModification); + objectModification.setAttribute("xmlns:c", "http://midpoint.evolveum.com/xml/ns/public/common/common-3"); + objectModification.setAttribute("xmlns", "http://midpoint.evolveum.com/xml/ns/public/common/api-types-3"); + objectModification.setAttribute("xmlns:t", "http://prism.evolveum.com/xml/ns/public/types-3"); + Element itemDelta = doc.createElement("itemDelta"); + objectModification.appendChild(itemDelta); + Element modificationType = doc.createElement("t:modificationType"); + modificationType.insertBefore(doc.createTextNode(midpointAction),modificationType.getLastChild()); + itemDelta.appendChild(modificationType); + Element modificationPath = doc.createElement("t:path"); + modificationPath.insertBefore(doc.createTextNode("c:assignment"),modificationPath.getLastChild()); + itemDelta.appendChild(modificationPath); + Element modificationValue = doc.createElement("t:value"); + + /* There are small differences between the XML + for a resource and a role. The two specific + examples I used to build these are: + https://github.com/Evolveum/midpoint/blob/master/samples/rest/modification-assign-account.xml + for the resource and: + https://github.com/Evolveum/midpoint/blob/master/samples/rest/modify-user-assign-role.xml + for the role. + */ + + if ( type.equals("resources")) { + Element modificationConstruction = doc.createElement("c:construction"); + modificationConstruction.setAttribute("xmlns:icfs", "http://midpoint.evolveum.com/xml/ns/public/connector/icf-1/resource-schema-3"); + Element modificationResource = doc.createElement("c:resourceRef"); + modificationResource.setAttribute("oid",resourceOrRoleOid); + modificationConstruction.appendChild(modificationResource); + modificationValue.appendChild(modificationConstruction); + } + else { + Element modificationRole = doc.createElement("c:targetRef"); + modificationRole.setAttribute("oid",resourceOrRoleOid); + modificationRole.setAttribute("type","c:RoleType"); + modificationValue.appendChild(modificationRole); + } + itemDelta.appendChild(modificationValue); + + final DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); + final DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS"); + final LSSerializer writer = impl.createLSSerializer(); + writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); + writer.getDomConfig().setParameter("xml-declaration", Boolean.TRUE); + + /* The below nonsense is to get a UTF-8 XML declaration, + the declaration defaulted to UTF-16 in my testing, + and sending a UTF-16 declaration to Midpoint threw + a 500 for me. This could just be a configuration issue. + */ + + LSOutput lsOutPut = impl.createLSOutput(); + lsOutPut.setEncoding("UTF-8"); + Writer stringWriter = new StringWriter(); + lsOutPut.setCharacterStream(stringWriter); + writer.write(doc,lsOutPut); + StringEntity stringEntity = new StringEntity(stringWriter.toString()); + httpPatch.setEntity(stringEntity); + CloseableHttpResponse response = client.execute(httpPatch); + if ( response.getStatusLine().getStatusCode() >= 200 && response.getStatusLine().getStatusCode() < 300 ) { + patchStatus = "Success " + response.getStatusLine().getStatusCode(); + } + else { + patchStatus = "Error " + response.getStatusLine().getStatusCode(); + } + + } catch (ClassNotFoundException CNFE) { + System.err.println(CNFE.getMessage()); + CNFE.printStackTrace(); + + } catch (IllegalAccessException IAE) { + System.err.println(IAE.getMessage()); + IAE.printStackTrace(); + + } catch (InstantiationException IE) { + System.err.println(IE.getMessage()); + IE.printStackTrace(); + + } catch (IOException IOE) { + System.err.println(IOE.getMessage()); + IOE.printStackTrace(); + + } catch (AuthenticationException AE) { + System.err.println(AE.getMessage()); + AE.printStackTrace(); + + } catch (ParserConfigurationException PCE) { + System.err.println(PCE.getMessage()); + PCE.printStackTrace(); + + } catch (Exception e) { + /* Shouldn't get here... */ + e.printStackTrace(); + } + + return new String(patchStatus); + } +} +