Skip to content

Commit

Permalink
Plugin property mapping w fixed tests (#33)
Browse files Browse the repository at this point in the history
* testing property mapping

* refining solution

* adding property resolver to both plugins

* add logic for workflowstep

* fixing typos

* adding option to use jvm proxy settings

* cleaning up

* allow job-level override

* fixing logic of overriding proxy settings from job level

* fix tests

* remove unnused import

* add java doc

* improve validation and log

* add unit test

---------

Co-authored-by: Jake Cohen <[email protected]>
Co-authored-by: jmanuelosunamoreno <[email protected]>
  • Loading branch information
3 people authored Aug 19, 2024
1 parent 24ad7e6 commit dcd109b
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 26 deletions.
123 changes: 109 additions & 14 deletions src/main/java/edu/ohio/ais/rundeck/HttpBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.dtolabs.rundeck.core.execution.workflow.steps.StepException;
import com.dtolabs.rundeck.core.execution.workflow.steps.StepFailureReason;
import com.dtolabs.rundeck.core.storage.ResourceMeta;
import com.dtolabs.rundeck.core.utils.IPropertyLookup;
import com.dtolabs.rundeck.plugins.PluginLogger;
import com.dtolabs.rundeck.plugins.step.PluginStepContext;
import com.google.gson.Gson;
Expand Down Expand Up @@ -88,13 +89,14 @@ public enum Reason implements FailureReason {
}


public CloseableHttpClient getHttpClient(Map<String, Object> options) throws GeneralSecurityException {
public CloseableHttpClient getHttpClient(Map<String, Object> options) throws GeneralSecurityException, StepException {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();

httpClientBuilder.disableAuthCaching();
httpClientBuilder.disableAutomaticRetries();

if(options.containsKey("sslVerify") && !Boolean.parseBoolean(options.get("sslVerify").toString())) {

if(!getBooleanOption(options, "sslVerify", true)) {
log.log(5,"Disabling all SSL certificate verification.");
SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
sslContextBuilder.loadTrustMaterial(null, new TrustStrategy() {
Expand All @@ -107,8 +109,27 @@ public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws Ce
httpClientBuilder.setSSLHostnameVerifier(new NoopHostnameVerifier());
httpClientBuilder.setSSLContext(sslContextBuilder.build());
}
if(options.containsKey("proxySettings") && Boolean.parseBoolean(options.get("proxySettings").toString())){
HttpHost proxy = new HttpHost(options.get("proxyIP").toString(), Integer.valueOf((String)options.get("proxyPort")), "http");
if(getBooleanOption(options, "useSystemProxySettings", false) && !getBooleanOption(options, "proxySettings", false)) {
log.log(5, "Using proxy settings set on system");
String proxyHost = System.getProperty("http.proxyHost");
String proxyPort = System.getProperty("http.proxyPort");
if (proxyPort.isEmpty() || proxyHost.isEmpty()) {
throw new StepException("proxyHost and proxyPort are required to use System Proxy Settings", StepFailureReason.ConfigurationFailure);
}
HttpHost proxy = new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http");
httpClientBuilder.setProxy(proxy);
}
if (getBooleanOption(options, "proxySettings", false)) {
String proxyIP = getStringOption(options, "proxyIP", "");
String proxyPort = getStringOption(options, "proxyPort", "");

if (proxyIP.isEmpty() || proxyPort.isEmpty()) {
throw new StepException("Proxy IP and Proxy Port are required to use Proxy Settings.", StepFailureReason.ConfigurationFailure);
}

log.log(5, "proxy IP set in job: " + proxyIP);
log.log(5, "proxy Port set in job: " + proxyPort);
HttpHost proxy = new HttpHost(proxyIP, Integer.parseInt(proxyPort), "http");
httpClientBuilder.setProxy(proxy);
}

Expand All @@ -132,19 +153,19 @@ public void doRequest(Map<String, Object> options, HttpUriRequest request, Integ
try {
response = this.getHttpClient(options).execute(request);

if(options.containsKey("printResponseCode") && Boolean.parseBoolean(options.get("printResponseCode").toString())) {
if(getBooleanOption(options,"printResponseCode",false)) {
String responseCode = response.getStatusLine().toString();
log.log(2, "Response Code: " + responseCode);
}

//print the response content
if(options.containsKey("printResponse") && Boolean.parseBoolean(options.get("printResponse").toString())) {
if(getBooleanOption(options,"printResponse",false)) {
output = getOutputForResponse(this.prettyPrint(response));
//print response
log.log(2, output);
}

if(options.containsKey("printResponseToFile") && Boolean.parseBoolean(options.get("printResponseToFile").toString())){
if(getBooleanOption(options,"printResponseToFile",false)){
File file = new File(options.get("file").toString());
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
if( output.isEmpty() ){
Expand All @@ -158,7 +179,7 @@ public void doRequest(Map<String, Object> options, HttpUriRequest request, Integ
}

//check response status
if(options.containsKey("checkResponseCode") && Boolean.parseBoolean(options.get("checkResponseCode").toString())) {
if(getBooleanOption(options,"checkResponseCode",false)) {

if(options.containsKey("responseCode")){
int responseCode = Integer.valueOf( (String) options.get("responseCode"));
Expand Down Expand Up @@ -335,14 +356,14 @@ public String prettyPrint(HttpResponse response){


String getAuthHeader(PluginStepContext pluginStepContext, Map<String, Object> options) throws StepException {
String authentication = options.containsKey("authentication") ? options.get("authentication").toString() : AUTH_NONE;
String authentication = getStringOption(options, "authentication",AUTH_NONE);
//moving the password to the key storage
String password=null;
String authHeader = null;


if(options.containsKey("password") ){
String passwordRaw = options.containsKey("password") ? options.get("password").toString() : null;
String passwordRaw = getStringOption(options, "password");
//to avid the test error add a try-catch
//if it didn't find the key path, it will use the password directly
byte[] content = SecretBundleUtil.getStoragePassword(pluginStepContext.getExecutionContext(),passwordRaw );
Expand All @@ -356,7 +377,7 @@ String getAuthHeader(PluginStepContext pluginStepContext, Map<String, Object> o

if(authentication.equals(AUTH_BASIC)) {
// Setup the authentication header for BASIC
String username = options.containsKey("username") ? options.get("username").toString() : null;
String username = getStringOption(options, "username");

if(username == null || password == null) {
throw new StepException("Username and password not provided for BASIC Authentication",
Expand All @@ -369,9 +390,12 @@ String getAuthHeader(PluginStepContext pluginStepContext, Map<String, Object> o
authHeader = "Basic " + com.dtolabs.rundeck.core.utils.Base64.encode(authHeader);
} else if (authentication.equals(AUTH_OAUTH2)) {
// Get an OAuth token and setup the auth header for OAuth
String tokenEndpoint = options.containsKey("oauthTokenEndpoint") ? options.get("oauthTokenEndpoint").toString() : null;
String validateEndpoint = options.containsKey("oauthValidateEndpoint") ? options.get("oauthValidateEndpoint").toString() : null;
String clientId = options.containsKey("username") ? options.get("username").toString() : null;
String tokenEndpoint = getStringOption(options, "oauthTokenEndpoint");
String validateEndpoint = getStringOption(options, "oauthValidateEndpoint");
String clientId = getStringOption(options, "username");



String clientSecret = password;


Expand Down Expand Up @@ -460,5 +484,76 @@ public void setHeaders(String headers, RequestBuilder request){
}
}

static void propertyResolver(String pluginType, String property, Map<String,Object> Configuration, PluginStepContext context, String SERVICE_PROVIDER_NAME) {

String projectPrefix = "project.plugin." + pluginType + "." + SERVICE_PROVIDER_NAME + ".";
String frameworkPrefix = "framework.plugin." + pluginType + "." + SERVICE_PROVIDER_NAME + ".";

Map<String,String> projectProperties = context.getFramework().getFrameworkProjectMgr().getFrameworkProject(context.getFrameworkProject()).getProperties();
IPropertyLookup frameworkProperties = context.getFramework().getPropertyLookup();

if(!Configuration.containsKey(property) && projectProperties.containsKey(projectPrefix + property)) {

Configuration.put(property, projectProperties.get(projectPrefix + property));

} else if (!Configuration.containsKey(property) && frameworkProperties.hasProperty(frameworkPrefix + property)) {

Configuration.put(property, frameworkProperties.getProperty(frameworkPrefix + property));

}
}

/**
* Retrieves a string value from the options map.
* If the key does not exist or the value is null, it returns null.
*
* @param options the map containing option keys and values
* @param key the key whose associated value is to be returned
* @return the string value associated with the specified key, or null if the key does not exist or the value is null
*/
static String getStringOption(Map<String, Object> options, String key) {
return getStringOption(options, key, null);
}

/**
* Retrieves a string value from the options map.
* If the key does not exist or the value is null, it returns the specified default value.
*
* @param options the map containing option keys and values
* @param key the key whose associated value is to be returned
* @param defValue the default value to return if the key does not exist or the value is null
* @return the string value associated with the specified key, or the default value if the key does not exist or the value is null
*/
static String getStringOption(Map<String, Object> options, String key, String defValue) {
return options.containsKey(key) && options.get(key) != null ? options.get(key).toString() : defValue;
}

/**
* Retrieves an integer value from the options map.
* If the key does not exist or the value is null, it returns the specified default value.
*
* @param options the map containing option keys and values
* @param key the key whose associated value is to be returned
* @param defValue the default value to return if the key does not exist or the value is null
* @return the integer value associated with the specified key, or the default value if the key does not exist or the value is null
* @throws NumberFormatException if the value cannot be parsed as an integer
*/
public static Integer getIntOption(Map<String, Object> options, String key, Integer defValue) {
return options.containsKey(key) && options.get(key) != null ? Integer.parseInt(options.get(key).toString()) : defValue;
}

/**
* Retrieves a boolean value from the options map.
* If the key does not exist or the value is null, it returns the specified default value.
*
* @param options the map containing option keys and values
* @param key the key whose associated value is to be returned
* @param defValue the default value to return if the key does not exist or the value is null
* @return the boolean value associated with the specified key, or the default value if the key does not exist or the value is null
*/
public static Boolean getBooleanOption(Map<String, Object> options, String key, Boolean defValue) {
return options.containsKey(key) && options.get(key) != null ? Boolean.parseBoolean(options.get(key).toString()) : defValue;
}


}
8 changes: 8 additions & 0 deletions src/main/java/edu/ohio/ais/rundeck/HttpDescription.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.dtolabs.rundeck.core.plugins.configuration.Describable;
import com.dtolabs.rundeck.core.plugins.configuration.Description;
import com.dtolabs.rundeck.core.plugins.configuration.PropertyScope;
import com.dtolabs.rundeck.core.plugins.configuration.StringRenderingConstants;
import com.dtolabs.rundeck.plugins.util.DescriptionBuilder;
import com.dtolabs.rundeck.plugins.util.PropertyBuilder;
Expand Down Expand Up @@ -165,6 +166,13 @@ public Description getDescription() {
.defaultValue("false")
.renderingOption(StringRenderingConstants.GROUP_NAME,"Print")
.build())
.property(PropertyBuilder.builder()
.title("System Proxy Settings")
.booleanType("useSystemProxySettings")
.description("Choose whether to use proxy settings set on the JVM.")
.defaultValue("false")
.scope(PropertyScope.Project)
.build())
.build();
}
}
26 changes: 19 additions & 7 deletions src/main/java/edu/ohio/ais/rundeck/HttpWorkflowNodeStepPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
import com.dtolabs.rundeck.core.execution.ExecutionContext;
import com.dtolabs.rundeck.core.execution.proxy.ProxySecretBundleCreator;
import com.dtolabs.rundeck.core.execution.proxy.SecretBundle;
import com.dtolabs.rundeck.core.execution.utils.ResolverUtil;
import com.dtolabs.rundeck.core.execution.workflow.steps.StepException;
import com.dtolabs.rundeck.core.execution.workflow.steps.StepFailureReason;
import com.dtolabs.rundeck.core.execution.workflow.steps.node.NodeStepException;
import com.dtolabs.rundeck.core.plugins.Plugin;
import com.dtolabs.rundeck.core.plugins.configuration.Describable;
import com.dtolabs.rundeck.core.plugins.configuration.Description;
import com.dtolabs.rundeck.core.utils.IPropertyLookup;
import com.dtolabs.rundeck.plugins.PluginLogger;
import com.dtolabs.rundeck.plugins.ServiceNameConstants;
import com.dtolabs.rundeck.plugins.descriptions.PluginProperty;
import com.dtolabs.rundeck.plugins.step.NodeStepPlugin;
import com.dtolabs.rundeck.plugins.step.PluginStepContext;
import com.dtolabs.rundeck.plugins.util.DescriptionBuilder;
import edu.ohio.ais.rundeck.util.OAuthClient;
import edu.ohio.ais.rundeck.util.SecretBundleUtil;
import org.apache.http.HttpEntity;
Expand All @@ -25,6 +29,10 @@
import java.io.UnsupportedEncodingException;
import java.util.*;

import static edu.ohio.ais.rundeck.HttpBuilder.propertyResolver;
import static edu.ohio.ais.rundeck.HttpBuilder.getIntOption;
import static edu.ohio.ais.rundeck.HttpBuilder.getStringOption;

@Plugin(name = HttpWorkflowNodeStepPlugin.SERVICE_PROVIDER_NAME, service = ServiceNameConstants.WorkflowNodeStep)
public class HttpWorkflowNodeStepPlugin implements NodeStepPlugin, Describable, ProxySecretBundleCreator {
public static final String SERVICE_PROVIDER_NAME = "edu.ohio.ais.rundeck.HttpWorkflowNodeStepPlugin";
Expand All @@ -40,7 +48,6 @@ public class HttpWorkflowNodeStepPlugin implements NodeStepPlugin, Describable,
*/
public static final Integer DEFAULT_TIMEOUT = 30*1000;


/**
* Synchronized map of all existing OAuth clients. This is indexed by
* the Client ID and the token URL so that we can store and re-use access tokens.
Expand All @@ -58,13 +65,17 @@ public Description getDescription() {
public void executeNodeStep(PluginStepContext context, Map<String, Object> configuration, INodeEntry entry) throws NodeStepException {
PluginLogger log = context.getLogger();

// Parse out the options
String remoteUrl = configuration.containsKey("remoteUrl") ? configuration.get("remoteUrl").toString() : null;
String method = configuration.containsKey("method") ? configuration.get("method").toString() : null;
Description description = new HttpDescription(SERVICE_PROVIDER_NAME, "HTTP Request Node Step", "Performs an HTTP request with or without authentication (per node)").getDescription();
description.getProperties().forEach(prop->
propertyResolver("WorkflowNodeStep", prop.getName(), configuration, context, SERVICE_PROVIDER_NAME)
);

Integer timeout = configuration.containsKey("timeout") ? Integer.parseInt(configuration.get("timeout").toString()) : DEFAULT_TIMEOUT;
String headers = configuration.containsKey("headers") ? configuration.get("headers").toString() : null;
String body = configuration.containsKey("body") ? configuration.get("body").toString() : null;
// Parse out the options
String remoteUrl = getStringOption(configuration, "remoteUrl");
String method = getStringOption(configuration, "method");
Integer timeout =getIntOption(configuration,"timeout", DEFAULT_TIMEOUT);
String headers = getStringOption(configuration, "headers");
String body = getStringOption(configuration, "body");

log.log(5, "remoteUrl: " + remoteUrl);
log.log(5, "method: " + method);
Expand Down Expand Up @@ -146,4 +157,5 @@ public SecretBundle prepareSecretBundleWorkflowNodeStep(ExecutionContext context
public List<String> listSecretsPathWorkflowNodeStep(ExecutionContext context, INodeEntry node, Map<String, Object> configuration) {
return SecretBundleUtil.getListSecrets(configuration);
}

}
19 changes: 14 additions & 5 deletions src/main/java/edu/ohio/ais/rundeck/HttpWorkflowStepPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
import java.io.*;
import java.util.*;

import static edu.ohio.ais.rundeck.HttpBuilder.propertyResolver;
import static edu.ohio.ais.rundeck.HttpBuilder.getIntOption;
import static edu.ohio.ais.rundeck.HttpBuilder.getStringOption;


/**
* Main implementation of the plugin. This will handle fetching
Expand Down Expand Up @@ -69,12 +73,17 @@ public Description getDescription() {
public void executeStep(PluginStepContext pluginStepContext, Map<String, Object> options) throws StepException {
PluginLogger log = pluginStepContext.getLogger();

Description description = new HttpDescription(SERVICE_PROVIDER_NAME, "HTTP Request Node Step", "Performs an HTTP request with or without authentication (per node)").getDescription();
description.getProperties().forEach(prop->
propertyResolver("WorkflowStep",prop.getName(), options, pluginStepContext, SERVICE_PROVIDER_NAME)
);

// Parse out the options
String remoteUrl = options.containsKey("remoteUrl") ? options.get("remoteUrl").toString() : null;
String method = options.containsKey("method") ? options.get("method").toString() : null;
Integer timeout = options.containsKey("timeout") ? Integer.parseInt(options.get("timeout").toString()) : DEFAULT_TIMEOUT;
String headers = options.containsKey("headers") ? options.get("headers").toString() : null;
String body = options.containsKey("body") ? options.get("body").toString() : null;
String remoteUrl = getStringOption(options, "remoteUrl");
String method = getStringOption(options, "method");
Integer timeout =getIntOption(options,"timeout", DEFAULT_TIMEOUT);
String headers = getStringOption(options, "headers");
String body = getStringOption(options, "body");

if(remoteUrl == null || method == null) {
throw new StepException("Remote URL and Method are required.", StepFailureReason.ConfigurationFailure);
Expand Down
Loading

0 comments on commit dcd109b

Please sign in to comment.