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}