Skip to content

Commit

Permalink
Support Script Security plugin and sandbox for CAS 1.0 (SECURITY-488)
Browse files Browse the repository at this point in the history
  • Loading branch information
fcrespel committed May 8, 2017
1 parent e552e82 commit 79a9bd1
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 40 deletions.
7 changes: 6 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ THE SOFTWARE.
</parent>

<artifactId>cas-plugin</artifactId>
<version>1.3.1-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
<packaging>hpi</packaging>
<name>CAS Plugin</name>
<url>http://wiki.jenkins-ci.org/display/JENKINS/CAS+Plugin</url>
Expand Down Expand Up @@ -140,6 +140,11 @@ THE SOFTWARE.
<artifactId>mailer</artifactId>
<version>1.16</version>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>script-security</artifactId>
<version>1.27</version>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
package org.jenkinsci.plugins.cas.protocols;

import groovy.lang.GroovyShell;
import groovy.lang.Script;
import hudson.Extension;
import hudson.Util;
import hudson.model.Descriptor;
import hudson.util.FormValidation;

import java.util.Collection;

import org.codehaus.groovy.control.CompilationFailedException;
import org.jasig.cas.client.validation.TicketValidator;
import org.jenkinsci.plugins.cas.CasProtocol;
import org.jenkinsci.plugins.cas.Messages;
import org.jenkinsci.plugins.cas.validation.Cas10RoleParsingTicketValidator;
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException;
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedClasspathException;
import org.jenkinsci.plugins.scriptsecurity.scripts.UnapprovedUsageException;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;

import hudson.Extension;
import hudson.Util;
import hudson.model.Descriptor;
import hudson.util.FormValidation;

/**
* CAS 1.0 protocol support.
*
Expand All @@ -27,11 +29,21 @@ public class Cas10Protocol extends CasProtocol {

public final String rolesValidationScript;
public final String testValidationResponse;
public final boolean sandbox;

@DataBoundConstructor
private final SecureGroovyScript secureRolesValidationScript;

@Deprecated
public Cas10Protocol(String rolesValidationScript, String testValidationResponse) {
this(rolesValidationScript, testValidationResponse, false);
}

@DataBoundConstructor
public Cas10Protocol(String rolesValidationScript, String testValidationResponse, boolean sandbox) {
this.rolesValidationScript = Util.fixEmptyAndTrim(rolesValidationScript);
this.testValidationResponse = Util.fixEmpty(testValidationResponse);
this.sandbox = sandbox;
this.secureRolesValidationScript = getSecureGroovyScript(this.rolesValidationScript, this.sandbox);
}

@Override
Expand All @@ -42,10 +54,14 @@ public String getAuthoritiesAttribute() {
@Override
public TicketValidator createTicketValidator(String casServerUrl) {
Cas10RoleParsingTicketValidator ticketValidator = new Cas10RoleParsingTicketValidator(casServerUrl);
ticketValidator.setRolesValidationScript(rolesValidationScript);
ticketValidator.setRolesValidationScript(secureRolesValidationScript);
return ticketValidator;
}

private static SecureGroovyScript getSecureGroovyScript(String script, boolean sandbox) {
return new SecureGroovyScript(script, sandbox, null).configuringWithKeyItem();
}

@Extension
public static final class DescriptorImpl extends Descriptor<CasProtocol> {
@Override
Expand All @@ -56,10 +72,10 @@ public String getDisplayName() {
@SuppressWarnings("rawtypes")
public FormValidation doTestScript(
@QueryParameter("rolesValidationScript") final String rolesValidationScript,
@QueryParameter("testValidationResponse") final String testValidationResponse) {
@QueryParameter("testValidationResponse") final String testValidationResponse,
@QueryParameter("sandbox") final boolean sandbox) {
try {
Script script = new GroovyShell().parse(rolesValidationScript);
Collection roles = Cas10RoleParsingTicketValidator.parseRolesFromValidationResponse(script, testValidationResponse);
Collection roles = Cas10RoleParsingTicketValidator.parseRolesFromValidationResponse(getSecureGroovyScript(rolesValidationScript, sandbox), testValidationResponse);
if (roles == null) {
return FormValidation.error(Messages.Cas10Protocol_rolesValidationScript_noResult());
}
Expand All @@ -68,6 +84,14 @@ public FormValidation doTestScript(
return FormValidation.error(Messages.Cas10Protocol_rolesValidationScript_compilationError() + ": " + e);
} catch (ClassCastException e) {
return FormValidation.error(Messages.Cas10Protocol_rolesValidationScript_returnTypeError() + ": " + e);
} catch (RejectedAccessException e) {
return FormValidation.error(Messages.Cas10Protocol_rolesValidationScript_rejectedAccessError() + ": " + e);
} catch (UnapprovedUsageException e) {
return FormValidation.error(Messages.Cas10Protocol_rolesValidationScript_unapprovedUsageError() + ": " + e);
} catch (UnapprovedClasspathException e) {

This comment has been minimized.

Copy link
@jglick

jglick May 9, 2017

Member

Will not be thrown, since you are not supporting classpaths.

return FormValidation.error(Messages.Cas10Protocol_rolesValidationScript_unapprovedClasspathError() + ": " + e);
} catch (Exception e) {
return FormValidation.error(Messages.Cas10Protocol_rolesValidationScript_unknownError() + ": " + e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package org.jenkinsci.plugins.cas.validation;

import groovy.lang.GroovyShell;
import groovy.lang.Script;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.authentication.AttributePrincipalImpl;
import org.jasig.cas.client.validation.AbstractCasProtocolUrlBasedTicketValidator;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.AssertionImpl;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript;

import groovy.lang.Binding;
import jenkins.model.Jenkins;

/**
* Implementation of a Ticket Validator that can validate tickets conforming to the CAS 1.0 specification.
Expand All @@ -31,9 +30,8 @@ public class Cas10RoleParsingTicketValidator extends AbstractCasProtocolUrlBased

public static final String DEFAULT_ROLE_ATTRIBUTE = "roles";

private String rolesValidationScript;
private SecureGroovyScript rolesValidationScript;
private String rolesAttribute = DEFAULT_ROLE_ATTRIBUTE;
private Script parsedScript;

public Cas10RoleParsingTicketValidator(final String casServerUrlPrefix) {
super(casServerUrlPrefix);
Expand Down Expand Up @@ -61,7 +59,7 @@ protected Assertion parseResponseFromServer(final String response) throws Ticket
reader.readLine();
final String name = reader.readLine();

List<String> roles = parseRolesFromValidationResponse(getParsedScript(), response);
List<String> roles = parseRolesFromValidationResponse(rolesValidationScript, response);
if (roles != null) {
Map<String, Object> attributes = new HashMap<String, Object>(1);
attributes.put(rolesAttribute, roles);
Expand All @@ -71,7 +69,7 @@ protected Assertion parseResponseFromServer(final String response) throws Ticket
return new AssertionImpl(name);
}

} catch (final IOException e) {
} catch (final Exception e) {

This comment has been minimized.

Copy link
@jglick

jglick May 9, 2017

Member

RejectedAccessException will need to be handled specially to report missing signatures for approval.

throw new TicketValidationException("Unable to parse response.", e);
}
}
Expand All @@ -83,13 +81,14 @@ protected Assertion parseResponseFromServer(final String response) throws Ticket
* @return list of roles
*/
@SuppressWarnings("rawtypes")
public static List<String> parseRolesFromValidationResponse(Script script, String response) {
public static List<String> parseRolesFromValidationResponse(SecureGroovyScript script, String response) throws Exception {
if (script == null)
return null;

// Run the script to parse the response
script.getBinding().setVariable("response", response);
Collection coll = (Collection) script.run();
Binding binding = new Binding();
binding.setVariable("response", response);
Collection coll = (Collection) script.evaluate(Jenkins.getInstance().getPluginManager().uberClassLoader, binding);
if (coll == null || coll.isEmpty())
return null;

Expand All @@ -104,30 +103,18 @@ public static List<String> parseRolesFromValidationResponse(Script script, Strin
return roles;
}

/**
* Get the parsed Groovy roles validation script.
* @return parsed Groovy script
*/
protected synchronized Script getParsedScript() {
if (parsedScript == null && StringUtils.isNotEmpty(rolesValidationScript)) {
parsedScript = new GroovyShell().parse(rolesValidationScript);
}
return parsedScript;
}

/**
* @return the rolesValidationScript
*/
public String getRolesValidationScript() {
public SecureGroovyScript getRolesValidationScript() {
return rolesValidationScript;
}

/**
* @param rolesValidationScript the rolesValidationScript to set
*/
public void setRolesValidationScript(String rolesValidationScript) {
public void setRolesValidationScript(SecureGroovyScript rolesValidationScript) {
this.rolesValidationScript = rolesValidationScript;
this.parsedScript = null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ Cas10Protocol.rolesValidationScript.result=Roles parsed from the test validation
Cas10Protocol.rolesValidationScript.noResult=Roles Validation Script returned no result
Cas10Protocol.rolesValidationScript.compilationError=Roles Validation Script failed to compile
Cas10Protocol.rolesValidationScript.returnTypeError=Roles Validation Script did not return a Collection
Cas10Protocol.rolesValidationScript.rejectedAccessError=Roles Validation Script uses forbidden language elements
Cas10Protocol.rolesValidationScript.unapprovedUsageError=Roles Validation Script is not approved for execution
Cas10Protocol.rolesValidationScript.unapprovedClasspathError=Roles Validation Script classpath is not approved
Cas10Protocol.rolesValidationScript.unknownError=Roles Validation Script could not be tested due to an unknown error
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ Cas10Protocol.rolesValidationScript.result=R
Cas10Protocol.rolesValidationScript.noResult=Le script de validation des rôles n''a retourné aucun résultat
Cas10Protocol.rolesValidationScript.compilationError=Le script de validation des rôles n''a pas pu être compilé
Cas10Protocol.rolesValidationScript.returnTypeError=Le script de validation des rôles n''a pas retourné de Collection
Cas10Protocol.rolesValidationScript.rejectedAccessError=Le script de validation des rôles utilise des éléments interdits du langage
Cas10Protocol.rolesValidationScript.unapprovedUsageError=Le script de validation des rôles n''est pas approuvé
Cas10Protocol.rolesValidationScript.unapprovedClasspathError=Le script de validation des rôles utilise un classpath non approuvé
Cas10Protocol.rolesValidationScript.unknownError=Le script de validation des rôles n''a pas pu être testé en raison d''une erreur inconnue
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
<f:entry title="${%rolesValidationScript}" field="rolesValidationScript">
<f:textarea />
</f:entry>
<f:entry title="${%sandbox}" field="sandbox">
<f:checkbox />

This comment has been minimized.

Copy link
@jglick

jglick May 9, 2017

Member

Recommend default="true" as practically everyone should be using the sandbox.

</f:entry>
<f:entry title="${%testValidationResponse}" field="testValidationResponse">
<f:textarea />
</f:entry>
<f:validateButton title="${%testScript}" method="testScript" with="rolesValidationScript,testValidationResponse" />
<f:validateButton title="${%testScript}" method="testScript" with="rolesValidationScript,testValidationResponse,sandbox" />
</f:advanced>
</j:jelly>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
description=<a href="{0}">CAS 1.0</a> is a text-based protocol. Custom extensions may provide support for roles, which can be parsed with a Groovy script.
rolesValidationScript=Roles Validation Script
testValidationResponse=Test Validation Response
sandbox=Use Groovy sandbox
testScript=Test Script
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
description=<a href="{0}">CAS 1.0</a> est un protocole basé sur du texte. Des extensions spécifiques peuvent apporter le support de rôles, qui peuvent être extraits à l''aide d''un script Groovy.
rolesValidationScript=Script de validation des rôles
testValidationResponse=Réponse de validation de test
sandbox=Utiliser la sandbox Groovy
testScript=Tester le script

0 comments on commit 79a9bd1

Please sign in to comment.