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 * This class provides a simple Webhooks destination 022 */ 023public class Webhooks extends SyncDestination { 024 025 public static final String URL_ARG = "url"; 026 public static final String CREATE_SUCCESS_CODES_ARG = "create-success-codes"; 027 public static final String MODIFY_SUCCESS_CODES_ARG = "modify-success-codes"; 028 public static final String DELETE_SUCCESS_CODES_ARG = "delete-success-codes"; 029 private URL url; 030 private List<Integer> createSuccessCodes; 031 private List<Integer> modifySuccessCodes; 032 private List<Integer> deleteSuccessCodes; 033 private SyncServerContext sc; 034 035 /** 036 * Provides the extension name for display purpose such as in manage-extension and dsconfig 037 * @return the extension name 038 */ 039 @Override 040 public String getExtensionName() { 041 return "sync-destination-webhooks"; 042 } 043 044 /** 045 * Provides a description for the extension for display purpose such as in manage-extension and dsconfig 046 * @return a list of descriptive paragraphs 047 */ 048 @Override 049 public String[] getExtensionDescription() { 050 return new String[]{"A webhooks destination."}; 051 } 052 053 /** 054 * Provides a descriptive endpoint URL for the instance of the extension 055 * @return the instance URL 056 */ 057 @Override 058 public String getCurrentEndpointURL() { 059 return null; 060 } 061 062 /** 063 * Performs the necessary processing to declare the arguments the extension requires in order to perform its task. 064 * @param parser the argument parse to which arguments are to be registered 065 * @throws ArgumentException in case any argument could not be registered 066 */ 067 @Override 068 public void defineConfigArguments(ArgumentParser parser) throws ArgumentException { 069 StringArgument urlArg = new StringArgument(null, URL_ARG, true, 1, "{url}", "URL of the webhook to publish changes to"); 070 urlArg.addValueValidator(new URLArgumentValueValidator()); 071 parser.addArgument(urlArg); 072 073 IntegerArgument createSuccessCodes = new IntegerArgument(null, CREATE_SUCCESS_CODES_ARG, false,0,"{http-code}","the list of HTTP result codes from the target to be considered to denote a successful operation",100,599, Arrays.asList(new Integer[]{200,201})); 074 parser.addArgument(createSuccessCodes); 075 076 IntegerArgument modifySuccessCodes = new IntegerArgument(null, MODIFY_SUCCESS_CODES_ARG, false,0,"{http-code}","the list of HTTP result codes from the target to be considered to denote a successful operation",100,599, Arrays.asList(new Integer[]{200})); 077 parser.addArgument(modifySuccessCodes); 078 079 IntegerArgument deleteSuccessCodes = new IntegerArgument(null, DELETE_SUCCESS_CODES_ARG, false,0,"{http-code}","the list of HTTP result codes from the target to be considered to denote a successful operation",100,599, Arrays.asList(new Integer[]{200,204})); 080 parser.addArgument(deleteSuccessCodes); 081 } 082 083 @Override 084 public void initializeSyncDestination(SyncServerContext serverContext, SyncDestinationConfig config, ArgumentParser parser) throws EndpointException { 085 this.sc = serverContext; 086 String destinationURL = parser.getStringArgument(URL_ARG).getValue(); 087 try { 088 url = new URL(destinationURL); 089 } catch (MalformedURLException e) { 090 throw new EndpointException(PostStepResult.ABORT_OPERATION,"The configured target URL ["+destinationURL+"] could not be parsed.",e); 091 } 092 createSuccessCodes = parser.getIntegerArgument(CREATE_SUCCESS_CODES_ARG).getValues(); 093 modifySuccessCodes = parser.getIntegerArgument(MODIFY_SUCCESS_CODES_ARG).getValues(); 094 deleteSuccessCodes = parser.getIntegerArgument(DELETE_SUCCESS_CODES_ARG).getValues(); 095 } 096 097 /** 098 * Performs the necessary processing to publish a creation event to a webhook target 099 * @param entryToCreate the entry to create 100 * @param operation the sync operation 101 * @throws EndpointException if any issue is encountered in the process of publishing the event 102 */ 103 @Override 104 public void createEntry(Entry entryToCreate, SyncOperation operation) throws EndpointException { 105 boolean publicationSuccess = publish(toJSON(operation,operation.getDestinationEntryAfterChange()), createSuccessCodes); 106 if (!publicationSuccess){ 107 throw new EndpointException(PostStepResult.RETRY_OPERATION_LIMITED); 108 } 109 } 110 111 /** 112 * Performs the necessary processing to publish a modification even to a webhook target 113 * @param entryToModify the entry to modify 114 * @param modsToApply the modifications to publish 115 * @param operation the sync operation 116 * @throws EndpointException if any issue is encountered in the process of publishing the event 117 */ 118 @Override 119 public void modifyEntry(Entry entryToModify, List<Modification> modsToApply, SyncOperation operation) throws EndpointException { 120 boolean publicationSuccess = publish(toJSON(operation,operation.getDestinationEntryAfterChange()),modifySuccessCodes); 121 if (!publicationSuccess){ 122 throw new EndpointException(PostStepResult.RETRY_OPERATION_LIMITED); 123 } 124 } 125 126 /** 127 * Performs the necessary processing to publish a deleting to a webhook target 128 * @param entryToDelete the entry to delete 129 * @param operation the sync operation 130 * @throws EndpointException if any issue is encountered in the process of publishing the event 131 */ 132 @Override 133 public void deleteEntry(Entry entryToDelete, SyncOperation operation) throws EndpointException { 134 boolean publicationSuccess = publish(toJSON(operation,entryToDelete),deleteSuccessCodes); 135 if (!publicationSuccess){ 136 throw new EndpointException(PostStepResult.RETRY_OPERATION_LIMITED); 137 } 138 } 139 140 private boolean publish(String json, List<Integer> successCodes) throws EndpointException { 141 Boolean result = Boolean.FALSE; 142 HttpURLConnection conn = null; 143 try { 144 conn = (HttpURLConnection) url.openConnection(); 145 conn.setRequestMethod("POST"); 146 conn.setDoOutput(true); 147 conn.setDoInput(true); 148 conn.setUseCaches(false); 149 conn.setAllowUserInteraction(false); 150 conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); 151 152 OutputStream out = conn.getOutputStream(); 153 Writer writer = new OutputStreamWriter(out, "UTF-8"); 154 writer.write(json); 155 writer.close(); 156 out.close(); 157 158 if (successCodes.contains(conn.getResponseCode()) ) { 159 throw new IOException(conn.getResponseMessage()); 160 } 161 162 // Buffer the result into a string 163 BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); 164 StringBuilder sb = new StringBuilder(); 165 String line; 166 while ((line = rd.readLine()) != null) { 167 sb.append(line); 168 } 169 rd.close(); 170 //Do something with the response 171 result = Boolean.TRUE; 172 } catch (IOException e) { 173 if (conn != null){ 174 conn.disconnect(); 175 } 176 throw new EndpointException(new LDAPException(ResultCode.OTHER, e)); 177 } finally { 178 if (conn != null){ 179 conn.disconnect(); 180 } 181 } 182 return result; 183 } 184 185 private static String toJSON(SyncOperation operation, Entry entry){ 186 StringBuilder sb = new StringBuilder(); 187 sb.append('{'); 188 sb.append(jsonKey("operation")); 189 sb.append(jsonValue(operation.getType().name())); 190 sb.append(','); 191 sb.append(jsonKey("entry")); 192 sb.append(toJSON(entry)); 193 sb.append('}'); 194 return sb.toString(); 195 } 196 197 private static String toJSON(Entry entry){ 198 StringBuilder sb = new StringBuilder(); 199 sb.append('{'); 200 sb.append(jsonKey("_dn")); 201 sb.append(jsonValue(entry.getDN())); 202 Collection<Attribute> attributes = entry.getAttributes(); 203 for (Attribute attribute: attributes){ 204 String[] values = attribute.getValues(); 205 if ( values != null && values.length>0) { 206 sb.append(','); 207 sb.append(jsonKey(attribute.getName())); 208 sb.append(jsonArray(values)); 209 } 210 } 211 sb.append('}'); 212 return sb.toString(); 213 } 214 215 private final static String jsonValue(final String s){ 216 final StringBuilder sb = new StringBuilder(); 217 sb.append('"'); 218 sb.append(s); 219 sb.append('"'); 220 return sb.toString(); 221 } 222 223 private final static String jsonKey(final String s){ 224 final StringBuilder sb = new StringBuilder(); 225 sb.append(jsonValue(s)); 226 sb.append(':'); 227 return sb.toString(); 228 } 229 230 private final static String jsonArray(final String[] ss){ 231 final StringBuilder sb = new StringBuilder(); 232 sb.append('['); 233 sb.append(jsonValue(String.join("\",\"",ss))); 234 sb.append(']'); 235 return sb.toString(); 236 } 237}