Skip to content

Commit

Permalink
[JENKINS-22834] Connect view with controller with ajax.
Browse files Browse the repository at this point in the history
  • Loading branch information
ikedam committed Aug 2, 2014
1 parent f02fb0d commit 780459f
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 27 deletions.
Expand Up @@ -51,11 +51,13 @@
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
Expand Down Expand Up @@ -87,6 +89,7 @@
XSTREAM2.alias("com.cloudbees.hudson.plugins.modeling.scripts.ScriptApproval$PendingSignature", PendingSignature.class);
// Current:
XSTREAM2.alias("scriptApproval", ScriptApproval.class);
XSTREAM2.alias("approvedClasspath", ApprovedClasspath.class);
XSTREAM2.alias("pendingScript", PendingScript.class);
XSTREAM2.alias("pendingSignature", PendingSignature.class);
XSTREAM2.alias("pendingClasspath", PendingClasspath.class);
Expand All @@ -97,6 +100,31 @@ public static ScriptApproval get() {
return Jenkins.getInstance().getExtensionList(RootAction.class).get(ScriptApproval.class);
}

/**
* Approved classpath
*
* It is treated only with the hash,
* but additional information is provided for convenience.
*/
@Restricted(NoExternalUse.class) // for use from Jelly
public static class ApprovedClasspath {
private final String hash;
private final String path;

public ApprovedClasspath(String hash, String path) {
this.hash = hash;
this.path = path;
}

public String getHash() {
return hash;
}

public String getPath() {
return path;
}
}

/** All scripts which are already approved, via {@link #hash}. */
private final Set<String> approvedScriptHashes = new TreeSet<String>();

Expand All @@ -106,8 +134,36 @@ public static ScriptApproval get() {
/** All sandbox signatures which are already whitelisted for ACL-only use, in {@link StaticWhitelist} format. */
private /*final*/ Set<String> aclApprovedSignatures;

/** All external classpaths allowed used for scripts. Keys are hash, values are path (used only for displaying convenience).*/
private /*final*/ Map<String, String> approvedClasspaths /*= new HashMap<String, String>()*/;
/** All external classpaths allowed used for scripts. Keys are hashes.*/
private /*final*/ Map<String, ApprovedClasspath> approvedClasspathMap /*= new LinkedHashMap<String, ApprovedClasspath>()*/;

protected Map<String, ApprovedClasspath> getApprovedClasspathMap() {
return approvedClasspathMap;
}

protected boolean hasApprovedClasspath(String hash) {
return getApprovedClasspathMap().containsKey(hash);
}

protected ApprovedClasspath getApprovedClasspath(String hash) {
return getApprovedClasspathMap().get(hash);
}

/**
* @param acp
* @return true if added
*/
protected boolean addApprovedClasspath(ApprovedClasspath acp) {
if (hasApprovedClasspath(acp.getHash())) {
return false;
}
getApprovedClasspathMap().put(acp.getHash(), acp);
return true;
}

protected void removeAllApprovedClasspath() {
getApprovedClasspathMap().clear();
}

@Restricted(NoExternalUse.class) // for use from Jelly
public static abstract class PendingThing {
Expand Down Expand Up @@ -192,6 +248,9 @@ public String getHash() {

/**
* A classpath requiring approval by an administrator.
*
* They are distinguished only with hashes,
* but other additional information is provided for users.
*/
@Restricted(NoExternalUse.class) // for use from Jelly
public static final class PendingClasspath extends PendingThing {
Expand Down Expand Up @@ -227,7 +286,39 @@ public String getPath() {

private final Set<PendingSignature> pendingSignatures = new LinkedHashSet<PendingSignature>();

private /*final*/ Set<PendingClasspath> pendingClasspaths /*= new LinkedHashSet<PendingClasspath>()*/;
private /*final*/ Map<String, PendingClasspath> pendingClasspathMap /*= new LinkedHashMap<String, PendingClasspath>()*/;

protected Map<String, PendingClasspath> getPendingClasspathMap() {
return pendingClasspathMap;
}

protected boolean hasPendingClasspath(String hash) {
return getPendingClasspathMap().containsKey(hash);
}

protected PendingClasspath getPendingClasspath(String hash) {
return getPendingClasspathMap().get(hash);
}

/**
* @param pcp
* @return true if added
*/
protected boolean addPendingClasspath(PendingClasspath pcp) {
if (hasPendingClasspath(pcp.getHash())) {
return false;
}
getPendingClasspathMap().put(pcp.getHash(), pcp);
return true;
}

/**
* @param hash
* @return true if removed
*/
protected boolean removePendingClasspath(String hash) {
return getPendingClasspathMap().remove(hash) != null;
}

public ScriptApproval() {
try {
Expand All @@ -239,11 +330,11 @@ public ScriptApproval() {
if (aclApprovedSignatures == null) {
aclApprovedSignatures = new TreeSet<String>();
}
if (approvedClasspaths == null) {
approvedClasspaths = new HashMap<String, String>();
if (approvedClasspathMap == null) {
approvedClasspathMap = new LinkedHashMap<String, ApprovedClasspath>();
}
if (pendingClasspaths == null) {
pendingClasspaths = new LinkedHashSet<PendingClasspath>();
if (pendingClasspathMap == null) {
pendingClasspathMap = new LinkedHashMap<String, PendingClasspath>();
}
}

Expand Down Expand Up @@ -387,14 +478,14 @@ public synchronized void configureingClasspath(@Nonnull String path, @Nonnull Ap
return;
}

if (!approvedClasspaths.containsKey(hash)) {
if (!hasApprovedClasspath(hash)) {
boolean shouldSave = false;
if (!Jenkins.getInstance().isUseSecurity() || (Jenkins.getAuthentication() != ACL.SYSTEM && Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS))) {
LOG.info(String.format("Classpath %s (%s) is approved as configured with RUN_SCRIPTS permission.", path, hash));
approvedClasspaths.put(hash, path);
addApprovedClasspath(new ApprovedClasspath(hash, path));
shouldSave = true;
} else {
if (pendingClasspaths.add(new PendingClasspath(path, hash, context))) {
if (addPendingClasspath(new PendingClasspath(path, hash, context))) {
LOG.info(String.format("%s (%s) is pended.", path, hash));
shouldSave = true;
}
Expand All @@ -418,9 +509,9 @@ public synchronized void configureingClasspath(@Nonnull String path, @Nonnull Ap
public synchronized boolean isClasspathApproved(@Nonnull String path) throws IOException {
String hash = hashClasspath(path);

String approvedPath = approvedClasspaths.get(hash);
ApprovedClasspath approvedPath = getApprovedClasspath(hash);
if (approvedPath != null) {
LOG.fine(String.format("%s (%s) has been approved as %s.", path, hash, approvedPath));
LOG.fine(String.format("%s (%s) has been approved as %s.", path, hash, approvedPath.getPath()));
}

return (approvedPath != null);
Expand Down Expand Up @@ -449,11 +540,11 @@ private static Item currentExecutingItem() {
public synchronized void checkClasspathApproved(@Nonnull String path) throws IOException, UnapprovedClasspathException {
String hash = hashClasspath(path);

if (!approvedClasspaths.containsKey(hash)) {
if (!hasApprovedClasspath(hash)) {
// Never approve classpath here.
ApprovalContext context = ApprovalContext.create();
context = context.withCurrentUser().withItemAsKey(currentExecutingItem());
if(pendingClasspaths.add(new PendingClasspath(path, hash, context))) {
if (addPendingClasspath(new PendingClasspath(path, hash, context))) {
LOG.info(String.format("%s (%s) is pended.", path, hash));
try {
save();
Expand All @@ -464,7 +555,7 @@ public synchronized void checkClasspathApproved(@Nonnull String path) throws IOE
throw new UnapprovedClasspathException(path, hash);
}

LOG.fine(String.format("%s (%s) has been approved as %s.", path, hash, approvedClasspaths.get(hash)));
LOG.fine(String.format("%s (%s) had been approved as %s.", path, hash, getApprovedClasspath(hash).getPath()));
}

/**
Expand Down Expand Up @@ -667,12 +758,45 @@ public Set<PendingSignature> getPendingSignatures() {
}

@Restricted(NoExternalUse.class) // for use from Jelly
public Map<String, String> getApprovedClasspaths() {
return approvedClasspaths;
public List<ApprovedClasspath> getApprovedClasspaths() {
return new ArrayList<ApprovedClasspath>(getApprovedClasspathMap().values());
}

@Restricted(NoExternalUse.class) // for use from Jelly
public Set<PendingClasspath> getPendingClasspaths() {
return pendingClasspaths;
public List<PendingClasspath> getPendingClasspaths() {
return new ArrayList<PendingClasspath>(getPendingClasspathMap().values());
}

@Restricted(NoExternalUse.class) // for use from AJAX
@JavaScriptMethod public List<ApprovedClasspath> approveClasspath(String hash, String classpath) throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
PendingClasspath cp = getPendingClasspath(hash);
if (cp != null && cp.getPath().equals(classpath)) {
removePendingClasspath(hash);
addApprovedClasspath(new ApprovedClasspath(cp.getHash(), cp.getPath()));
save();
}
return getApprovedClasspaths();
}

@Restricted(NoExternalUse.class) // for use from AJAX
@JavaScriptMethod public List<ApprovedClasspath> denyClasspath(String hash, String classpath) throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
PendingClasspath cp = getPendingClasspath(hash);
if (cp != null && cp.getPath().equals(classpath)) {
removePendingClasspath(hash);
save();
}
return getApprovedClasspaths();
}

// TODO nicer would be to allow the user to actually edit the list directly (with syntax checks)
@Restricted(NoExternalUse.class) // for use from AJAX
@JavaScriptMethod public synchronized List<ApprovedClasspath> clearApprovedClasspaths() throws IOException {
Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS);
removeAllApprovedClasspath();
save();
return getApprovedClasspaths();
}

}
Expand Up @@ -25,7 +25,7 @@ THE SOFTWARE.

<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:entry field="path" title="${%Path to Jarfile}">
<f:entry field="path" title="${%Path to Jarfile or class directory}">
<f:textbox />
</f:entry>
<f:entry title="">
Expand Down
Expand Up @@ -69,6 +69,29 @@ THE SOFTWARE.
updateApprovedSignatures(r);
});
}
function hideClasspath(hash) {
$('pcp-' + hash).style.display = 'none';
}
function updateApprovedClasspaths(r) {
$('approvedClasspaths').value = r.responseObject().map(function(e){return e.hash + "(" + e.path + ")";}).join('\n');
}
function approveClasspath(hash, path) {
mgr.approveClasspath(hash, path, function(r) {
updateApprovedClasspaths(r);
hideClasspath(hash);
});
}
function denyClasspath(hash, path) {
mgr.denyClasspath(hash, path, function(r) {
updateApprovedClasspaths(r);
hideClasspath(hash);
});
}
function clearApprovedClasspaths() {
mgr.clearApprovedClasspaths(function(r) {
updateApprovedClasspaths(r);
});
}
</script>
<j:choose>
<j:when test="${it.pendingScripts.isEmpty()}">
Expand Down Expand Up @@ -135,20 +158,20 @@ THE SOFTWARE.
</p>
</j:when>
<j:otherwise>
<j:forEach var="s" items="${it.pendingClasspaths}">
<div id="s-${s.hash}">
<j:forEach var="pcp" items="${it.pendingClasspaths}">
<div id="pcp-${pcp.hash}">
<p>
<button onclick="approveClasspath('${s.path}', '${s.hash}')">Approve</button> /
<button onclick="denyClasspath('${s.path}', '${s.hash}')">Deny</button>
${s.path} (${s.hash})
<button onclick="approveClasspath('${pcp.hash}', '${pcp.path}')">Approve</button> /
<button onclick="denyClasspath('${pcp.hash}', '${pcp.path}')">Deny</button>
${pcp.hash} (${pcp.path})
</p>
</div>
</j:forEach>
</j:otherwise>
</j:choose>
<p>Classpaths already approved:</p>
<textarea readonly="readonly" id="approvedClasspaths" rows="10" cols="80">
<j:forEach var="s" items="${it.approvedClasspaths.entrySet}">${s.value} (${s.key})<st:out value="&#10;"/></j:forEach>
<j:forEach var="acp" items="${it.approvedClasspaths}">${acp.hash} (${acp.path})<st:out value="&#10;"/></j:forEach>
</textarea>
<p>
You can also remove all previous classpaths approvals:
Expand Down

0 comments on commit 780459f

Please sign in to comment.