001package com.identityworksllc.iiq.common;
002
003import org.apache.commons.logging.Log;
004import org.apache.commons.logging.LogFactory;
005import sailpoint.connector.RPCService;
006import sailpoint.object.*;
007import sailpoint.tools.GeneralException;
008import sailpoint.tools.Util;
009
010import java.io.IOException;
011import java.io.InputStream;
012import java.lang.reflect.Constructor;
013import java.util.ArrayList;
014import java.util.HashMap;
015import java.util.List;
016import java.util.Map;
017import java.util.concurrent.atomic.AtomicBoolean;
018
019/**
020 * Utilities for interacting with the IQService, particularly for executing Powershell
021 */
022public class IQServiceUtilities {
023    /**
024     * A fluent builder for {@link RpcRequest} objects
025     */
026    @SuppressWarnings("javadoc")
027    public static class RPCRequestBuilder {
028        /**
029         * The connection info
030         */
031        private Map<String, Object> connectionInfo;
032
033        /**
034         * The account request to pass to the Powershell script
035         */
036        private ProvisioningPlan.AccountRequest inputs;
037
038        /**
039         * The name of the RPC service to call, usually ScriptExecutor
040         */
041        private String rpcService;
042
043        /**
044         * The Rule to serialize and execute, often constructed ad hoc
045         */
046        private Rule rule;
047
048        /**
049         * The rule template to use if {@link #withCommands(String)} is used.
050         */
051        private String template;
052
053        private RPCRequestBuilder() {
054            this.rpcService = SCRIPT_EXECUTOR;
055        }
056
057        /**
058         * Creates a new {@link RPCRequestBuilder} object
059         * @return A new RPCRequestBuilder, ready for fluent building
060         */
061        public static RPCRequestBuilder builder() {
062            return new RPCRequestBuilder();
063        }
064
065        /**
066         * Builds a new RpcRequest using the inputs present, if enough are available.
067         * If the inputs are not correct, throws an exception.
068         *
069         * @return the new RpcRequest
070         * @throws GeneralException if input parameters are invalid or the creation of the request fails
071         */
072        public RpcRequest build() throws GeneralException {
073            if (this.inputs == null) {
074                throw new GeneralException("The builder was not supplied with input parameters or an AccountRequest");
075            }
076            if (this.rule == null) {
077                throw new GeneralException("The builder was not supplied with a Rule to execute");
078            }
079            if (this.connectionInfo == null || this.connectionInfo.isEmpty()) {
080                throw new GeneralException("The builder was not supplied with populated connection parameters");
081            }
082            if (!this.connectionInfo.containsKey(RPCService.IQSERVICE_CONFIGURATION) && !this.connectionInfo.containsKey(RPCService.CONFIG_IQSERVICE_HOST)) {
083                throw new GeneralException("The connection info is not valid IQService connection info; must contain either " + RPCService.IQSERVICE_CONFIGURATION + " or " + RPCService.CONFIG_IQSERVICE_HOST);
084            }
085            return new RpcRequest(this.rpcService, RUN_AFTER_SCRIPT, createRPCRequestContent(this.rule, this.connectionInfo, this.inputs));
086        }
087
088        public RPCRequestBuilder withCommands(String commands, String template) throws GeneralException {
089            return withTemplate(template).withRule(wrapPowershellCommands(commands, this.template));
090        }
091
092        public RPCRequestBuilder withCommands(String commands) throws GeneralException {
093            return withRule(wrapPowershellCommands(commands, this.template));
094        }
095
096        public RPCRequestBuilder withConnectionInfo(Map<String, Object> connectionInfo) {
097            this.connectionInfo = connectionInfo;
098            return this;
099        }
100
101        public RPCRequestBuilder withConnectionInfo(Application application) {
102            return withConnectionInfo(application.getAttributes());
103        }
104
105        public RPCRequestBuilder withInputs(ProvisioningPlan.AccountRequest inputs) {
106            this.inputs = inputs;
107            return this;
108        }
109
110        public RPCRequestBuilder withInputs(Map<String, Object> inputs) {
111            return withInputs(createFakeAccountRequest(inputs));
112        }
113
114        public RPCRequestBuilder withRPCService(String serviceName) {
115            this.rpcService = serviceName;
116            return this;
117        }
118
119        public RPCRequestBuilder withRule(Rule rule) {
120            this.rule = rule;
121            return this;
122        }
123
124        public RPCRequestBuilder withTemplate(String template) {
125            this.template = template;
126            return this;
127        }
128    }
129    /**
130     * The 'hidden' input to disable hostname verification
131     */
132    public static final String CONFIG_DISABLE_HOSTNAME_VERIFICATION = "disableHostnameVerification";
133    /**
134     * The output variable returned by the default template
135     */
136    public static final String DEFAULT_TEMPLATE_OUTPUT = "output";
137    /**
138     * The input argument containing the Application's attributes
139     */
140    public static final String IQSERVICE_FIELD_APPLICATION = "Application";
141    /**
142     * The input argument containing an {@link sailpoint.object.ProvisioningPlan.AccountRequest}
143     */
144    public static final String IQSERVICE_FIELD_REQUEST = "Request";
145    /**
146     * The input argument containing the actual Powershell script to run
147     */
148    public static final String IQSERVICE_FIELD_RULE = "postScript";
149    /**
150     * The default Powershell script type
151     */
152    public static final String RUN_AFTER_SCRIPT = "runAfterScript";
153    /**
154     * The default RPC Service type, execution of a Powershell script
155     */
156    public static final String SCRIPT_EXECUTOR = "ScriptExecutor";
157
158    /**
159     * The path to the standard powershell template, which should be included with this library
160     */
161    public static final String STANDARD_POWERSHELL_TEMPLATE = "/powershell.template.ps1";
162    /**
163     * The token in the wrapper script to replace with the user commands
164     */
165    public static final String TOKEN_USER_COMMAND = "%%USER_COMMAND%%";
166    /**
167     * Logger
168     */
169    private static final Log log = LogFactory.getLog(IQServiceUtilities.class);
170    /**
171     * The cached static result of the {@link #supportsTLS()} check
172     */
173    private static final AtomicBoolean supportsTLS;
174
175    static {
176        boolean result = false;
177        try {
178            Constructor<RPCService> ignored = RPCService.class.getConstructor(String.class, Integer.TYPE, Boolean.TYPE, Boolean.TYPE);
179            result = true;
180        } catch(Exception e) {
181            /* Ignore */
182        }
183        supportsTLS = new AtomicBoolean(result);
184    }
185
186    /**
187     * Checks the {@link RpcResponse} for error messages. If there are any, it throws an exception with the messages.
188     * If the RpcResponse contains any messages, they will be logged as warnings.
189     *
190     * The default logger will be used. If you wish to provide your own logger, use the two-argument
191     * {@link #checkRpcFailure(RpcResponse, Log)}.
192     *
193     * @param response the response to check
194     * @return the same response
195     * @throws GeneralException if there were any errors
196     */
197    protected static RpcResponse checkRpcFailure(RpcResponse response) throws GeneralException {
198        return checkRpcFailure(response, log);
199    }
200
201    /**
202     * Checks the RpcResponse for error messages. If there are any, it throws an exception with the messages.
203     * If the RpcResponse contains any messages, they will be logged as warnings.
204     *
205     * @param response the response to check
206     * @param yourLogger The logger ot which the errors should be logged
207     * @return the same response
208     * @throws GeneralException if there were any errors
209     */
210    public static RpcResponse checkRpcFailure(RpcResponse response, Log yourLogger) throws GeneralException {
211        if (response == null) {
212            return null;
213        }
214        if (response.getErrors() != null && !response.getErrors().isEmpty()) {
215            if (yourLogger != null) {
216                yourLogger.error("Received errors from the IQService: " + response.getErrors());
217            } else {
218                log.error("Received errors from the IQService: " + response.getErrors());
219            }
220            throw new GeneralException("Errors from IQService: " + response.getErrors().toString());
221        }
222        if (response.getMessages() != null) {
223            for(String message : response.getMessages()) {
224                yourLogger.warn("Received a message from the IQService: " + message);
225            }
226        }
227        return response;
228    }
229
230    /**
231     * Constructs a fake account request to use for a call to IIQ
232     *
233     * @param parameters Any parameters to send to the IIQ call
234     * @return The account request
235     */
236    public static ProvisioningPlan.AccountRequest createFakeAccountRequest(Map<String, Object> parameters) {
237        ProvisioningPlan.AccountRequest accountRequest = new ProvisioningPlan.AccountRequest();
238        accountRequest.setApplication("PSUTIL");
239        accountRequest.setNativeIdentity("*FAKE*");
240        accountRequest.setOperation(ProvisioningPlan.AccountRequest.Operation.Modify);
241        List<ProvisioningPlan.AttributeRequest> fakeAttributeRequests = new ArrayList<ProvisioningPlan.AttributeRequest>();
242        ProvisioningPlan.AttributeRequest attributeRequest = new ProvisioningPlan.AttributeRequest();
243        attributeRequest.setName("CmdletResponse");
244        attributeRequest.setOperation(ProvisioningPlan.Operation.Add);
245        attributeRequest.setValue("");
246        fakeAttributeRequests.add(attributeRequest);
247        if (parameters != null) {
248            for (String key : parameters.keySet()) {
249                ProvisioningPlan.AttributeRequest fakeAttribute = new ProvisioningPlan.AttributeRequest();
250                fakeAttribute.setOperation(ProvisioningPlan.Operation.Add);
251                fakeAttribute.setName(key);
252                fakeAttribute.setValue(parameters.get(key));
253                fakeAttributeRequests.add(fakeAttribute);
254            }
255        }
256        accountRequest.setAttributeRequests(fakeAttributeRequests);
257
258        if (log.isTraceEnabled()) {
259            log.trace("For input arguments " + parameters + ", produced output " + Utilities.safeToXml(accountRequest));
260        }
261
262        return accountRequest;
263    }
264
265    /**
266     * Creates a map to be passed to an RPCRequest.
267     *
268     * @param rule The rule to execute
269     * @param connectionInfo The Application containing connection into
270     * @param inputs The rule inputs
271     * @return The populated Map
272     */
273    public static Map<String, Object> createRPCRequestContent(Rule rule, Application connectionInfo, Map<String, Object> inputs) {
274        return createRPCRequestContent(rule, connectionInfo.getAttributes(), inputs);
275    }
276
277    /**
278     * Creates a map to be passed to an RPCRequest.
279     *
280     * @param rule The rule to execute
281     * @param connectionInfo The Map containing connection into
282     * @param inputs The rule inputs
283     * @return The populated Map
284     */
285    public static Map<String, Object> createRPCRequestContent(Rule rule, Map<String, Object> connectionInfo, ProvisioningPlan.AccountRequest inputs) {
286        Map<String, Object> dataMap = new HashMap<>();
287
288        dataMap.put(IQSERVICE_FIELD_RULE, rule);
289        dataMap.put(IQSERVICE_FIELD_APPLICATION, connectionInfo);
290        dataMap.put(IQSERVICE_FIELD_REQUEST, inputs);
291
292        return dataMap;
293    }
294
295    /**
296     * Creates a map to be passed to an RPCRequest.
297     *
298     * @param rule The rule to execute
299     * @param connectionInfo The Map containing connection into
300     * @param inputs The rule inputs
301     * @return The populated Map
302     */
303    public static Map<String, Object> createRPCRequestContent(Rule rule, Map<String, Object> connectionInfo, Map<String, Object> inputs) {
304        return createRPCRequestContent(rule, connectionInfo, createFakeAccountRequest(inputs));
305    }
306
307    /**
308     * Creates a map to be passed to an RPCRequest.
309     *
310     * @param commands The Powershell commands to execute; will be translated via {@link #wrapPowershellCommands(String, String)}
311     * @param connectionInfo The Application containing connection into
312     * @param inputs The rule inputs
313     * @return The populated Map
314     * @throws GeneralException if constructing request input fails, usually because of failure to read the template
315     */
316    public static Map<String, Object> createRPCRequestContent(String commands, Application connectionInfo, Map<String, Object> inputs) throws GeneralException {
317        return createRPCRequestContent(wrapPowershellCommands(commands, null), connectionInfo.getAttributes(), inputs);
318    }
319
320    /**
321     * Constructs an RPCService from the connection info provided
322     *
323     * @param connectionInfo The connection info from an Application or other config
324     * @return The resulting RPCService
325     */
326    public static RPCService createRPCService(Map<String, Object> connectionInfo) {
327        RPCService service;
328
329        Map<String, Object> iqServiceConfig = findIQServiceConfig(connectionInfo);
330        boolean ignoreHostnameVerification = Util.otob(iqServiceConfig.get(CONFIG_DISABLE_HOSTNAME_VERIFICATION));
331        boolean useTLS = Util.otob(iqServiceConfig.get(RPCService.CONFIG_IQSERVICE_TLS));
332
333        String iqServiceHost = Util.otoa(iqServiceConfig.get(RPCService.CONFIG_IQSERVICE_HOST));
334        int iqServicePort = Util.otoi(iqServiceConfig.get(RPCService.CONFIG_IQSERVICE_PORT));
335
336        if (log.isDebugEnabled()) {
337            log.debug("Creating new RPCService for host = " + iqServiceHost + ", port = " + iqServicePort);
338        }
339
340        // If you pass true, we're going to assume you know what you're talking about
341        if (ignoreHostnameVerification || useTLS || supportsTLS()) {
342            service = new RPCService(iqServiceHost, iqServicePort, false, useTLS, ignoreHostnameVerification);
343            service.setConnectorServices(new sailpoint.connector.DefaultConnectorServices());
344        } else {
345            service = new RPCService(iqServiceHost, iqServicePort, false);
346        }
347        return service;
348    }
349
350    /**
351     * Finds the IQService config. This is isolated here so that we can ignore warnings on
352     * as small a bit of code as possible
353     * @param connectionInfo The connection info from the application or other config
354     * @return The IQService config, either the original, or extracted
355     */
356    @SuppressWarnings("unchecked")
357    public static Map<String, Object> findIQServiceConfig(Map<String, Object> connectionInfo) {
358        Map<String, Object> iqServiceConfig = connectionInfo;
359        if (connectionInfo.get(RPCService.IQSERVICE_CONFIGURATION) instanceof Map) {
360            iqServiceConfig = (Map<String, Object>) connectionInfo.get(RPCService.IQSERVICE_CONFIGURATION);
361        }
362        return iqServiceConfig;
363    }
364
365    /**
366     * Get standard template from the classpath
367     * @return The standard template
368     * @throws IOException If any errors occur opening the template stream
369     * @throws GeneralException If any errors occur parsing the template stream
370     */
371    public static String getStandardTemplate() throws IOException, GeneralException {
372        try(InputStream templateStream = IQServiceUtilities.class.getResourceAsStream(STANDARD_POWERSHELL_TEMPLATE)) {
373            if (templateStream != null) {
374                return Util.readInputStream(templateStream);
375            }
376        }
377        return null;
378    }
379
380    /**
381     * Returns true if the RPCService in this instance of IIQ supports TLS.
382     *
383     * The TLS four and five-argument constructors to IQService are available
384     * in the following IIQ versions and higher:
385     *
386     *  - 8.0 GA
387     *  - 7.3p3
388     *  - 7.2p4
389     *  - 7.1p7
390     *
391     * This result is cached.
392     *
393     * @return True if it supports TLS, false otherwise
394     */
395    public static boolean supportsTLS() {
396        return supportsTLS.get();
397    }
398
399    /**
400     * Wraps Powershell commands into a Rule, substituting it into a template wrapper.
401     * This {@link Rule} should not be saved, but should be serialized directly with
402     * the input to the RPCService.
403     *
404     * @param commands The commands to execute in Powershell
405     * @param ruleTextTemplate The rule text template, or null to use the included default
406     * @return The resulting Rule object
407     * @throws GeneralException if constructing the new Rule fails
408     */
409    public static Rule wrapPowershellCommands(String commands, String ruleTextTemplate) throws GeneralException {
410        String finalRuleText;
411        try {
412            String template = ruleTextTemplate;
413            if (Util.isNullOrEmpty(template)) {
414                template = getStandardTemplate();
415            }
416            if (Util.isNullOrEmpty(template)) {
417                throw new IllegalArgumentException("No Powershell template is available");
418            }
419            finalRuleText = template.replace(TOKEN_USER_COMMAND, commands);
420        } catch(IOException e) {
421            throw new GeneralException("Could not read Powershell template", e);
422        }
423        if (Util.isNotNullOrEmpty(finalRuleText)) {
424            Rule pretendPowershellRule = new Rule();
425            pretendPowershellRule.setName("Powershell Rule Template Wrapper " + System.currentTimeMillis());
426            pretendPowershellRule.setSource(finalRuleText);
427            pretendPowershellRule.setAttribute("ObjectOrientedScript", "true");
428            pretendPowershellRule.setAttribute("disabled", "false");
429            pretendPowershellRule.setAttribute("extension", ".ps1");
430            pretendPowershellRule.setAttribute("program", "powershell.exe");
431            pretendPowershellRule.setAttribute("powershellTimeout", 10);
432
433            if (log.isTraceEnabled()) {
434                log.trace("Transformed commands into Rule: " + pretendPowershellRule.toXml());
435            }
436
437            return pretendPowershellRule;
438        } else {
439            throw new GeneralException(new IllegalStateException("The final generated Powershell rule text is null or empty; is the template on the class path?"));
440        }
441    }
442}