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}