001package com.pingidentity.sync.destination;
002
003import com.unboundid.directory.sdk.sync.api.SyncDestination;
004import com.unboundid.directory.sdk.sync.config.SyncDestinationConfig;
005import com.unboundid.directory.sdk.sync.types.EndpointException;
006import com.unboundid.directory.sdk.sync.types.PostStepResult;
007import com.unboundid.directory.sdk.sync.types.SyncOperation;
008import com.unboundid.directory.sdk.sync.types.SyncServerContext;
009import com.unboundid.ldap.sdk.*;
010import com.unboundid.util.args.*;
011
012import java.io.*;
013import java.net.HttpURLConnection;
014import java.net.MalformedURLException;
015import java.net.URL;
016import java.util.Arrays;
017import java.util.Collection;
018import java.util.List;
019
020/**
021 * Implements a class to trigger singularkey flow or policy
022 */
023public class Singularkey extends SyncDestination {
024  public static final String FLOW_URL_ARG = "flow-url";
025  public static final String API_KEY_ARG = "api-key";
026  public static final String SUCCESS_CODE_ARG = "success-code";
027  private URL url;
028  private String apiKey;
029  private List<Integer> successCodes;
030
031  /**
032   * Provides the extension name
033   * @return the extension name
034   */
035  @Override
036  public String getExtensionName() {
037    return "Singularkey destination";
038  }
039
040  /**
041   * Provides a description for the extension for display purpose in the dsconfig and manage-extension CLIs
042   * @return a list of descriptive strings, each of ow which is displayed as a paragraph
043   */
044  @Override
045  public String[] getExtensionDescription() {
046    return new String[] {
047        "Start a flow or policy in Singularkey"
048    };
049  }
050
051  /**
052   * Performs the necessary processing to define the arguments necessary for the extension to work
053   * @param parser the argument parse
054   * @throws ArgumentException if any issue is encountered attempting to register arguments with the parser
055   */
056  @Override
057  public void defineConfigArguments(final ArgumentParser parser)
058      throws ArgumentException {
059    IntegerArgument successCodesArg = new IntegerArgument(null, SUCCESS_CODE_ARG,false,0,"{http-code}","HTTP codes to treat as success", Arrays.asList(new Integer[]{200,201,202,204}));
060    parser.addArgument(successCodesArg);
061    StringArgument apiUrlArg = new StringArgument(null, FLOW_URL_ARG, true, 1, "{url}", "The base URL for the instance of SingularKey to trigger flows in. Example: https://example.singularkey.com/v1/company/<companyID>/flows/<flowID>/start");
062    apiUrlArg.addValueValidator(new URLArgumentValueValidator());
063    parser.addArgument(apiUrlArg);
064    StringArgument apKeylArg = new StringArgument(null, API_KEY_ARG, true, 1, "{url}", "The API key. This is from the SingularKey App. Not the flow need to be registered with the app before the flow can be started.");
065    parser.addArgument(apKeylArg);
066  }
067
068  /**
069   * Performs the necessary processing to initialize the extension
070   * such as verifying the provided configuration arguments
071   * @param serverContext the server context
072   * @param config the configuration object for the instance of the extension
073   * @param parser the argument parser
074   * @throws EndpointException if any issue is encountered whilst attempting to initialize the instance of the extension
075   */
076  @Override
077  public void initializeSyncDestination(SyncServerContext serverContext, SyncDestinationConfig config, ArgumentParser parser)
078          throws EndpointException {
079    successCodes = parser.getIntegerArgument(SUCCESS_CODE_ARG).getValues();
080    apiKey = parser.getStringArgument(API_KEY_ARG).getValue();
081    try {
082      url = new URL(parser.getStringArgument(FLOW_URL_ARG).getValue());
083    } catch (MalformedURLException e) {
084      throw new EndpointException(PostStepResult.ABORT_OPERATION,e);
085    }
086  }
087
088  /**
089   * Not supported for this notification-only extension
090   * @param destEntryMappedFromSrc the computed entry based on the entry retrieved from the source and all mappings configured in the applicable sync class
091   * @param operation the sync operation
092   * @return a list of entries retrieved based on the provided data
093   * @throws EndpointException always, as this extension only supports notification mode
094   */
095  @Override
096  public List<Entry> fetchEntry(Entry destEntryMappedFromSrc, SyncOperation operation)
097          throws EndpointException {
098    throw new EndpointException(PostStepResult.ABORT_OPERATION,"This sync destination only supports notification mode");
099  }
100
101  /**
102   * Provides information about the instance of the extension
103   * @return the configured singularkey URL
104   */
105  @Override
106  public String getCurrentEndpointURL() {
107    return url.toString();
108  }
109
110  /**
111   * performs the necessary processing to handle the event of an entry creation at the source
112   * @param entryToCreate the entry to create
113   * @param operation the sync operation
114   * @throws EndpointException in case the event could not be published at the destination
115   */
116  @Override
117  public void createEntry(Entry entryToCreate, SyncOperation operation)
118          throws EndpointException {
119    String payload = toJSON(operation, entryToCreate);
120    publish(payload, successCodes);
121  }
122
123  /**
124   * Performs the necessary processing to handle the event of an entry modification at the source
125   * @param entryToModify the entry to modify
126   * @param modsToApply the list of modifications to apply
127   * @param operation the sync operation
128   * @throws EndpointException in case the event could not be published at the destination
129   */
130  @Override
131  public void modifyEntry(Entry entryToModify, List<Modification> modsToApply, SyncOperation operation)
132          throws EndpointException {
133    String payload = toJSON(operation, operation.getDestinationEntryAfterChange());
134    publish(payload, successCodes);
135  }
136
137  /**
138   * Performs the necessary processing to handle the event of an entry deletion at the source
139   * @param entryToDelete the entry to delete
140   * @param operation the sync operation
141   * @throws EndpointException in case the event could not be published at the destination
142   */
143  @Override
144  public void deleteEntry(Entry entryToDelete, SyncOperation operation)
145          throws EndpointException {
146    String payload = toJSON(operation, entryToDelete);
147    publish(payload, successCodes);
148  }
149
150  /**
151   * Performs the necessary processing to publish a JSON payload
152   * @param json the payload in valid JSON format
153   * @param successCodes a list of codes that indicate a successful publication at the destination
154   * @return true if the publication was successful, false otherwise
155   * @throws EndpointException
156   */
157  private boolean publish(String json, List<Integer> successCodes) throws EndpointException {
158    Boolean result = Boolean.FALSE;
159    HttpURLConnection conn = null;
160    try {
161      conn = (HttpURLConnection) url.openConnection();
162      conn.setRequestMethod("POST");
163      conn.setDoOutput(true);
164      conn.setDoInput(true);
165      conn.setUseCaches(false);
166      conn.setAllowUserInteraction(false);
167      conn.setRequestProperty("Content-Type", "application/json");
168      conn.setRequestProperty("X-SK-API-Key", apiKey);
169
170      OutputStream out = conn.getOutputStream();
171      Writer writer = new OutputStreamWriter(out, "UTF-8");
172      writer.write(json);
173      writer.close();
174      out.close();
175
176      if (!successCodes.contains(conn.getResponseCode()) ) {
177        throw new IOException("The response code received from the server ["+conn.getResponseCode()+"] did not match any of the expected codes ("+successCodes+")");
178      }
179
180      // Buffer the result into a string
181      BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
182      StringBuilder sb = new StringBuilder();
183      String line;
184      while ((line = rd.readLine()) != null) {
185        sb.append(line);
186      }
187      rd.close();
188      //Do something with the response
189      result = Boolean.TRUE;
190    } catch (IOException e) {
191      if (conn != null){
192        conn.disconnect();
193      }
194      throw new EndpointException(new LDAPException(ResultCode.OTHER, e));
195    } finally {
196      if (conn != null){
197        conn.disconnect();
198      }
199    }
200    return result;
201  }
202
203  /**
204   * Performs the necessary processing to format a sync operation and its corresponding entry
205   * into a valid payload for the purpose of handling in a flow
206   * @param operation the sync operation
207   * @param entry the entry
208   * @return the JSON string
209   */
210  private static String toJSON(SyncOperation operation, Entry entry){
211    StringBuilder json = new StringBuilder();
212    json.append('{');
213    json.append(jsonKey("event"));
214    json.append(jsonValue("synchronization"));
215    json.append(',');
216    json.append(jsonKey("type"));
217    json.append(jsonValue(operation.getType().toString()));
218    json.append(',');
219    json.append(jsonKey("entry"));
220    json.append(toJSON(entry));
221    json.append('}');
222    return json.toString();
223  }
224
225  /**
226   * Performs the necessary processing to wrap an entry in JSON format
227   * @param entry the entry to convert to JSON
228   * @return the JSON string
229   */
230  private static String toJSON(Entry entry){
231    StringBuilder sb = new StringBuilder();
232    sb.append('{');
233    sb.append(jsonKey("_dn"));
234    sb.append(jsonValue(entry.getDN()));
235    Collection<Attribute> attributes = entry.getAttributes();
236    for (Attribute attribute: attributes){
237      String[] values = attribute.getValues();
238      if ( values != null && values.length>0) {
239        sb.append(',');
240        sb.append(jsonKey(attribute.getName()));
241        sb.append(jsonArray(values));
242      }
243    }
244    sb.append('}');
245    return sb.toString();
246  }
247
248  /**
249   * Performs the necessary processing to quote a JSON value
250   * @param s the string to quote
251   * @return the quote string
252   */
253  private final static String jsonValue(final String s){
254    final StringBuilder sb = new StringBuilder();
255    sb.append('"');
256    sb.append(s);
257    sb.append('"');
258    return sb.toString();
259  }
260
261  /**
262   * Performs the necessary processing to quote a JSON key and append a colon
263   * @param s the string to use as key
264   * @return the resulting string
265   */
266  private final static String jsonKey(final String s){
267    final StringBuilder sb = new StringBuilder();
268    sb.append(jsonValue(s));
269    sb.append(':');
270    return sb.toString();
271  }
272
273  private final static String jsonArray(final String[] ss){
274    final StringBuilder sb = new StringBuilder();
275    sb.append('[');
276    sb.append(jsonValue(String.join("\",\"",ss)));
277    sb.append(']');
278    return sb.toString();
279  }
280}