From cbfedb6f8640952a5d591f3b2f93ae823fc959cd Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Mon, 25 May 2020 18:01:48 +0200 Subject: [PATCH 01/21] [JENKINS-62448] Enhance information displayed in approval page --- README.md | 2 +- .../scripts/ApprovalContext.java | 20 + .../scripts/ScriptApproval.java | 432 ++++++++++++++++-- .../scripts/languages/LanguageHelper.java | 61 +++ .../scripts/metadata/FullScriptMetadata.java | 230 ++++++++++ .../metadata/HashAndFullScriptMetadata.java | 40 ++ .../scripts/metadata/MetadataStorage.java | 232 ++++++++++ .../scripts/Messages.properties | 7 + .../scripts/ScriptApproval/_resources.css | 113 +++++ .../scripts/ScriptApproval/_resources.js | 215 +++++++++ .../scripts/ScriptApproval/index.jelly | 254 +--------- .../scripts/ScriptApproval/index.properties | 3 + .../scripts/ScriptApproval/tab-custom.jelly | 57 +++ .../ScriptApproval/tab-custom.properties | 1 + .../ScriptApproval/tab-with-body.jelly | 54 +++ .../ScriptApproval/tabs/_scriptContent.jelly | 10 + .../tabs/classPath_approved.jelly | 75 +++ .../tabs/classPath_pending.jelly | 74 +++ .../tabs/fullScript_approved.jelly | 204 +++++++++ .../tabs/fullScript_approved.properties | 63 +++ .../tabs/fullScript_pending.jelly | 117 +++++ .../tabs/fullScript_pending.properties | 38 ++ .../tabs/signature_approved.jelly | 70 +++ .../tabs/signature_pending.jelly | 70 +++ .../scripts/ScriptApproval/taglib | 0 25 files changed, 2166 insertions(+), 276 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/LanguageHelper.java create mode 100644 src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/FullScriptMetadata.java create mode 100644 src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/HashAndFullScriptMetadata.java create mode 100644 src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.js create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/_scriptContent.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly create mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/taglib diff --git a/README.md b/README.md index c24aa5bcc..f1668fad5 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ StaticWhitelist.from and loading a text file listing whitelisted methods. ### Classpath for evaluating scripts When constructing a GroovyShell to evaluate a script, or calling -`ecureGroovyScript.evaluate`, you must pass a `ClassLoader` which represents the effective +`secureGroovyScript.evaluate`, you must pass a `ClassLoader` which represents the effective classpath for the script. You could use the loader of Jenkins core, or your plugin, or `Jenkins.getInstance().getPluginManager().uberClassLoader`. diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java index 33abb9411..a398148df 100644 --- a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ApprovalContext.java @@ -31,6 +31,8 @@ import jenkins.model.Jenkins; import org.kohsuke.stapler.DataBoundConstructor; +import java.util.Objects; + /** * Represents background information about who requested that a script or signature be approved and for what purpose. * When created from a thread that generally carries authentication, such as within a {@link DataBoundConstructor}, be sure to use {@link #withCurrentUser}. @@ -125,4 +127,22 @@ public ApprovalContext withItemAsKey(@CheckForNull Item item) { return new ApprovalContext(user, n, n); } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApprovalContext that = (ApprovalContext) o; + return Objects.equals(user, that.user) && + Objects.equals(item, that.item) && + Objects.equals(key, that.key); + } + + @Override + public int hashCode() { + return Objects.hash(user, item, key); + } } diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java index 5bce32055..60be34ef9 100644 --- a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java @@ -24,10 +24,14 @@ package org.jenkinsci.plugins.scriptsecurity.scripts; +import com.google.common.annotations.VisibleForTesting; +import hudson.util.HttpResponses; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import net.sf.json.JSONArray; import net.sf.json.JSONObject; +import org.acegisecurity.Authentication; +import org.apache.commons.lang.StringUtils; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.AclAwareWhitelist; import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.ProxyWhitelist; @@ -54,9 +58,9 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.Date; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -68,15 +72,29 @@ import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.annotation.concurrent.GuardedBy; +import javax.servlet.ServletException; + import jenkins.model.Jenkins; import net.sf.json.JSON; import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; +import org.jenkinsci.plugins.scriptsecurity.scripts.languages.LanguageHelper; +import org.jenkinsci.plugins.scriptsecurity.scripts.metadata.FullScriptMetadata; +import org.jenkinsci.plugins.scriptsecurity.scripts.metadata.HashAndFullScriptMetadata; +import org.jenkinsci.plugins.scriptsecurity.scripts.metadata.MetadataStorage; +import org.jvnet.localizer.Localizable; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.bind.JavaScriptMethod; +import org.kohsuke.stapler.interceptor.RequirePOST; +import org.kohsuke.stapler.json.JsonBody; /** * Manages approved scripts. @@ -101,6 +119,9 @@ public class ScriptApproval extends GlobalConfiguration implements RootAction { XSTREAM2.alias("pendingClasspathEntry", PendingClasspathEntry.class); } + public static final String METADATA_GATHERING_PROP_NAME = ScriptApproval.class.getName() + ".metadataGathering"; + public static /* final */ boolean METADATA_GATHERING = Boolean.parseBoolean(System.getProperty(METADATA_GATHERING_PROP_NAME, "true")); + @Override protected XmlFile getConfigFile() { return new XmlFile(XSTREAM2, new File(Jenkins.getInstance().getRootDir(),getUrlName() + ".xml")); @@ -120,6 +141,119 @@ public GlobalConfigurationCategory getCategory() { return instance; } + // Used by Jelly + @Restricted(NoExternalUse.class) + public static final class Tab { + /** + * HTML safe + */ + public final String parameterName; + /** + * No path traversal possible + */ + public final String viewName; + + private final Localizable i18nCode; + + private Tab(String parameterName, String viewName, Localizable i18nCode) { + this.parameterName = parameterName; + this.viewName = viewName; + this.i18nCode = i18nCode; + } + + // used by Jelly + public String getI18nName() { + return this.i18nCode.toString(); + } + } + + private static final Tab FULL_SCRIPT_PENDING = new Tab("fullScriptPending", "fullScript_pending.jelly", Messages._ScriptApproval_tab_fullScriptPending()); + private static final Tab FULL_SCRIPT_APPROVED = new Tab("fullScriptApproved", "fullScript_approved.jelly", Messages._ScriptApproval_tab_fullScriptApproved()); + private static final Tab SIGNATURE_PENDING = new Tab("signaturePending", "signature_pending.jelly", Messages._ScriptApproval_tab_signaturePending()); + private static final Tab SIGNATURE_APPROVED = new Tab("signatureApproved", "signature_approved.jelly", Messages._ScriptApproval_tab_signatureApproved()); + private static final Tab CLASS_PATH_PENDING = new Tab("classPathPending", "classPath_pending.jelly", Messages._ScriptApproval_tab_classPathPending()); + private static final Tab CLASS_PATH_APPROVED = new Tab("classPathApproval", "classPath_approved.jelly", Messages._ScriptApproval_tab_classPathApproved()); + + private static final Tab[] ALL_TABS = new Tab[]{ + FULL_SCRIPT_PENDING, FULL_SCRIPT_APPROVED, + SIGNATURE_PENDING, SIGNATURE_APPROVED, + CLASS_PATH_PENDING, CLASS_PATH_APPROVED + }; + + private static final int FULL_SCRIPT_PENDING_INDEX = 0; + private static final int FULL_SCRIPT_APPROVED_INDEX = 1; + private static final int SIGNATURE_PENDING_INDEX = 2; + private static final int SIGNATURE_APPROVED_INDEX = 3; + private static final int CLASS_PATH_PENDING_INDEX = 4; + private static final int CLASS_PATH_APPROVED_INDEX = 5; + + // Used by Jelly + @Restricted(NoExternalUse.class) + public static final class TabInfo { + public Tab tab; + public int numOfNotification; + public boolean primaryColor; + public boolean active; + + private TabInfo(Tab tab) { + this.tab = tab; + } + } + + @Restricted(DoNotUse.class) // Web only + public void doIndex(StaplerRequest req, StaplerResponse rsp, @QueryParameter("tab") String tab) throws IOException, ServletException { + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + + TabInfo[] tabInfos = new TabInfo[ALL_TABS.length]; + Tab activeTab = null; + for (int i = 0; i < ALL_TABS.length; i++) { + Tab t = ALL_TABS[i]; + TabInfo info = new TabInfo(t); + if (t.parameterName.equals(tab)) { + activeTab = t; + info.active = true; + } + tabInfos[i] = info; + } + + if (activeTab == null) { + if (!StringUtils.isBlank(tab)) { + LOG.log(Level.FINER, "Invalid tab name received: {0}, redirecting to default one", tab); + } + for (int i = 0; i < ALL_TABS.length && activeTab == null; i++) { + if (tabInfos[i].numOfNotification > 0) { + tabInfos[i].active = true; + activeTab = tabInfos[i].tab; + } + } + if (activeTab == null) { + tabInfos[0].active = true; + activeTab = tabInfos[0].tab; + } + } + + tabInfos[FULL_SCRIPT_PENDING_INDEX].numOfNotification = pendingScripts.size(); + tabInfos[FULL_SCRIPT_PENDING_INDEX].primaryColor = true; + + tabInfos[FULL_SCRIPT_APPROVED_INDEX].numOfNotification = approvedScriptHashes.size(); + + tabInfos[SIGNATURE_PENDING_INDEX].numOfNotification = pendingSignatures.size(); + tabInfos[SIGNATURE_PENDING_INDEX].primaryColor = true; + + tabInfos[SIGNATURE_APPROVED_INDEX].numOfNotification = approvedSignatures.size(); + + tabInfos[CLASS_PATH_PENDING_INDEX].numOfNotification = pendingClasspathEntries.size(); + tabInfos[CLASS_PATH_PENDING_INDEX].primaryColor = true; + + tabInfos[CLASS_PATH_APPROVED_INDEX].numOfNotification = approvedClasspathEntries.size(); + + // will be injected as a variable inside Jelly view + req.setAttribute("activeTab", activeTab); + req.setAttribute("tabInfos", tabInfos); + + req.getView(this, "index.jelly").forward(req, rsp); + } + /** * Approved classpath entry. * @@ -165,6 +299,7 @@ boolean isClassDirectory() { } /** All scripts which are already approved, via {@link #hash}. */ + @GuardedBy("this") private final TreeSet approvedScriptHashes = new TreeSet(); /** All sandbox signatures which are already whitelisted, in {@link StaticWhitelist} format. */ @@ -180,6 +315,21 @@ boolean isClassDirectory() { approvedClasspathEntries.add(acp); } + @Restricted(NoExternalUse.class) // for use from Jelly + public synchronized List getApprovedFullScriptMetadata() { + List result = metadataStorage.getMetadataUsingHashes(this.approvedScriptHashes); + + // last used entries at the top + // then last approved entries + result.sort( + Comparator.comparingLong((HashAndFullScriptMetadata item) -> -item.metadata.getLastTimeUsed()) + .thenComparing((HashAndFullScriptMetadata item) -> -item.metadata.getLastApprovalTime()) + .thenComparing((HashAndFullScriptMetadata item) -> item.hash) + ); + + return result; + } + @Restricted(NoExternalUse.class) // for use from Jelly public static abstract class PendingThing { @@ -188,14 +338,29 @@ public static abstract class PendingThing { private @Nonnull ApprovalContext context; + private long approvalRequestTime; + PendingThing(@Nonnull ApprovalContext context) { this.context = context; + this.approvalRequestTime = new Date().getTime(); } public @Nonnull ApprovalContext getContext() { return context; } + public long getApprovalRequestTime() { + return approvalRequestTime; + } + + public @CheckForNull Date getApprovalRequestTimeDate() { + if (approvalRequestTime <= 0) { + // for legacy + return null; + } + return new Date(approvalRequestTime); + } + private Object readResolve() { if (user != null) { context = ApprovalContext.create().withUser(user); @@ -219,20 +384,16 @@ public String getHash() { return hash(script, language); } public Language getLanguage() { - for (Language l : ExtensionList.lookup(Language.class)) { - if (l.getName().equals(language)) { - return l; - } - } - return new Language() { - @Override public String getName() { - return language; - } - @Override public String getDisplayName() { - return ""; - } - }; + return LanguageHelper.getLanguageFromName(language); + } + + /** + * Prevent the transformation to Language if the name is sufficient + */ + public String getLanguageName() { + return this.language; } + @Override public int hashCode() { return script.hashCode() ^ language.hashCode(); } @@ -340,9 +501,13 @@ private PendingClasspathEntry getPendingClasspathEntry(@Nonnull String hash) { pendingClasspathEntries.add(pcp); } + @GuardedBy("this") + private transient MetadataStorage metadataStorage; + @DataBoundConstructor public ScriptApproval() { load(); + this.metadataStorage = new MetadataStorage("scriptApproval/scripts"); /* can be null when upgraded from old versions.*/ if (aclApprovedSignatures == null) { aclApprovedSignatures = new TreeSet(); @@ -367,14 +532,14 @@ public ScriptApproval() { } /** Nothing has ever been approved or is pending. */ - boolean isEmpty() { + synchronized boolean isEmpty() { return approvedScriptHashes.isEmpty() && - approvedSignatures.isEmpty() && - aclApprovedSignatures.isEmpty() && - approvedClasspathEntries.isEmpty() && - pendingScripts.isEmpty() && - pendingSignatures.isEmpty() && - pendingClasspathEntries.isEmpty(); + approvedSignatures.isEmpty() && + aclApprovedSignatures.isEmpty() && + approvedClasspathEntries.isEmpty() && + pendingScripts.isEmpty() && + pendingSignatures.isEmpty() && + pendingClasspathEntries.isEmpty(); } private static String hash(String script, String language) { @@ -438,6 +603,12 @@ public synchronized String configuring(@Nonnull String script, @Nonnull Language if (!approvedScriptHashes.contains(hash)) { if (!Jenkins.getInstance().isUseSecurity() || Jenkins.getAuthentication() != ACL.SYSTEM && Jenkins.getInstance().hasPermission(Jenkins.RUN_SCRIPTS)) { approvedScriptHashes.add(hash); + + if (METADATA_GATHERING) { + this.metadataStorage.withMetadata(hash, script, metadata -> + metadata.notifyApprovalDuringConfiguring(script, language, context) + ); + } } else { String key = context.getKey(); if (key != null) { @@ -465,7 +636,7 @@ public synchronized String configuring(@Nonnull String script, @Nonnull Language public synchronized String using(@Nonnull String script, @Nonnull Language language) throws UnapprovedUsageException { if (script.length() == 0) { // As a special case, always consider the empty script preapproved, as this is usually the default for new fields, - // and in many cases there is some sensible behavior for an emoty script which we want to permit. + // and in many cases there is some sensible behavior for an empty script which we want to permit. return script; } String hash = hash(script, language.getName()); @@ -473,6 +644,13 @@ public synchronized String using(@Nonnull String script, @Nonnull Language langu // Probably need not add to pendingScripts, since generally that would have happened already in configuring. throw new UnapprovedUsageException(hash); } + + if (METADATA_GATHERING) { + this.metadataStorage.withMetadata(hash, script, metadata -> + metadata.notifyUsage(script, language) + ); + } + return script; } @@ -603,13 +781,25 @@ public synchronized FormValidation checking(@Nonnull String script, @Nonnull Lan /** * Unconditionally approve a script. * Does no access checks and does not automatically save changes to disk. - * Useful mainly for testing. + * Useful mainly for testing. + * 2020-04-17, this method is used only in tests, except for CommandLauncher constructor + * * @param script the text of a possibly novel script * @param language the language in which it is written * @return {@code script}, for convenience */ public synchronized String preapprove(@Nonnull String script, @Nonnull Language language) { - approvedScriptHashes.add(hash(script, language.getName())); + String hash = hash(script, language.getName()); + approvedScriptHashes.add(hash); + + if (METADATA_GATHERING) { + String userLogin = this.getCurrentUserLogin(); + + this.metadataStorage.withMetadata(hash, script, metadata -> + metadata.notifyPreapproveSingle(script, language, userLogin) + ); + } + return script; } @@ -617,10 +807,20 @@ public synchronized String preapprove(@Nonnull String script, @Nonnull Language * Unconditionally approves all pending scripts. * Does no access checks and does not automatically save changes to disk. * Useful mainly for testing in combination with {@code @LocalData}. + *

+ * 2020-04-17, this method is used only in tests. */ public synchronized void preapproveAll() { + String userLogin = this.getCurrentUserLogin(); + for (PendingScript ps : pendingScripts) { - approvedScriptHashes.add(ps.getHash()); + String hash = ps.getHash(); + approvedScriptHashes.add(hash); + if (METADATA_GATHERING) { + this.metadataStorage.withMetadata(hash, ps.script, metadata -> + metadata.notifyPreapproveAll(ps, userLogin) + ); + } } pendingScripts.clear(); } @@ -728,17 +928,47 @@ String[][] reconfigure() throws IOException { return "scriptApproval"; } + @Override + public @Nonnull String getDisplayName() { + return "Script Approval"; + } + + @Deprecated @Restricted(NoExternalUse.class) // for use from Jelly public Set getPendingScripts() { return pendingScripts; } - @Restricted(NoExternalUse.class) // for use from AJAX - @JavaScriptMethod public void approveScript(String hash) throws IOException { + @Restricted(NoExternalUse.class) // for use from Jelly + public synchronized List getPendingScriptsSorted() { + List result = new ArrayList<>(pendingScripts.size()); + result.addAll(this.pendingScripts); + result.sort( + Comparator.comparingLong((PendingScript pendingScript) -> -pendingScript.getApprovalRequestTime()) + .thenComparing(PendingScript::getHash) + ); + return result; + } + + // @JavaScriptMethod, no longer accessible by JavaScript but still kept for compatibility + @VisibleForTesting + @Restricted(NoExternalUse.class) + public void approveScript(String hash) { Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + + String userLogin = this.getCurrentUserLogin(); + synchronized (this) { approvedScriptHashes.add(hash); - removePendingScript(hash); + PendingScript pendingScript = removePendingScript(hash); + + if (METADATA_GATHERING) { + String script = pendingScript == null ? null : pendingScript.script; + this.metadataStorage.withMetadata(hash, script, metadata -> + metadata.notifyApproval(pendingScript, userLogin) + ); + } + save(); } SecurityContext orig = ACL.impersonate(ACL.SYSTEM); @@ -751,24 +981,143 @@ public Set getPendingScripts() { } } - @Restricted(NoExternalUse.class) // for use from AJAX - @JavaScriptMethod public synchronized void denyScript(String hash) throws IOException { + // @JavaScriptMethod, no longer accessible by JavaScript but still kept for compatibility + @VisibleForTesting + @Restricted(NoExternalUse.class) + synchronized void denyScript(String hash) { Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); approvedScriptHashes.remove(hash); removePendingScript(hash); save(); } - private synchronized void removePendingScript(String hash) { + @RequirePOST + @Restricted(NoExternalUse.class) + public void doScriptContent(StaplerRequest req, StaplerResponse rsp, @QueryParameter(required = true) String hash) throws Exception { + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + + synchronized (this) { + FullScriptMetadata metadata = this.metadataStorage.getExisting(hash); + if (metadata == null) { + // this can occur naturally only if you have concurrent view on the scriptApproval page + LOG.log(Level.FINER, "Requesting a non-existing metadata {0}", hash); + throw HttpResponses.notFound(); + } + Language language = metadata.getLanguage(); + if (language != null && metadata.getScriptLength() > 0) { + String script = this.metadataStorage.readScript(hash); + if (script != null) { + req.setAttribute("script", script); + req.setAttribute("languageCodeMirrorMode", language.getCodeMirrorMode()); + req.getView(this, "tabs/_scriptContent.jelly").forward(req, rsp); + return; + } else { + // expected for legacy approved scripts (until first usage) + LOG.log(Level.FINER, "Requesting a non-existing script {0}", hash); + } + } + throw HttpResponses.notFound(); + } + } + + @Restricted(NoExternalUse.class) + public static final class AllSelectedHashesModel { + public String[] hashes; + } + + @RequirePOST + @Restricted(NoExternalUse.class) + public void doApprovePendingScripts(@JsonBody AllSelectedHashesModel content) { + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + LOG.log(Level.FINE, "Approval granted for selected {0}", content.hashes); + + synchronized (this) { + String userLogin = this.getCurrentUserLogin(); + + for (String hash : content.hashes) { + approvedScriptHashes.add(hash); + PendingScript pendingScript = removePendingScript(hash); + + if (METADATA_GATHERING) { + String script = pendingScript == null ? null : pendingScript.script; + this.metadataStorage.withMetadata(hash, script, metadata -> + metadata.notifyApproval(pendingScript, userLogin) + ); + } + } + + save(); + } + SecurityContext orig = ACL.impersonate(ACL.SYSTEM); + try { + for (ApprovalListener listener : ExtensionList.lookup(ApprovalListener.class)) { + for (String hash : content.hashes) { + listener.onApproved(hash); + } + } + } finally { + SecurityContextHolder.setContext(orig); + } + } + + /** + * Called on pending scripts + */ + @RequirePOST + @Restricted(NoExternalUse.class) + public void doDenyPendingScripts(@JsonBody AllSelectedHashesModel content) { + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + LOG.log(Level.FINE, "Deny pending scripts for selected {0}", content.hashes); + // at this point, no such script should be approved + synchronized (this) { + for (String hash : content.hashes) { + approvedScriptHashes.remove(hash); + removePendingScript(hash); + } + + save(); + } + } + + @RequirePOST + @Restricted(NoExternalUse.class) + public synchronized void doRevokeApprovalForScripts(@JsonBody AllSelectedHashesModel content) { + Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); + LOG.log(Level.FINE, "Approval revocation for selected {0}", content.hashes); + + for (String hash : content.hashes) { + approvedScriptHashes.remove(hash); + // at this point, no such script should be pending + removePendingScript(hash); + + if (METADATA_GATHERING) { + this.metadataStorage.removeHash(hash); + } + } + + save(); + } + + /** + * @return the script corresponding to the hash otherwise {@code null} + */ + private synchronized @CheckForNull PendingScript removePendingScript(String hash) { Iterator it = pendingScripts.iterator(); while (it.hasNext()) { - if (it.next().getHash().equals(hash)) { + PendingScript curr = it.next(); + if (curr.getHash().equals(hash)) { it.remove(); - break; + return curr; } } + + return null; } + /** + * @deprecated use {@link #doRevokeApprovalForScripts(AllSelectedHashesModel)} instead for better granularity + */ + @Deprecated @Restricted(NoExternalUse.class) // for use from AJAX @JavaScriptMethod public synchronized void clearApprovedScripts() throws IOException { Jenkins.getInstance().checkPermission(Jenkins.RUN_SCRIPTS); @@ -943,4 +1292,21 @@ public synchronized JSON clearApprovedClasspathEntries() throws IOException { return getClasspathRenderInfo(); } + /** + * To have effectively final variable + */ + private @CheckForNull String getCurrentUserLogin() { + String userLogin = null; + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth != null) { + userLogin = auth.getName(); + } + + return userLogin; + } + + @Restricted(NoExternalUse.class) // for jelly only + public boolean isMetadataGatheringEnabled() { + return METADATA_GATHERING; + } } diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/LanguageHelper.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/LanguageHelper.java new file mode 100644 index 000000000..acb9e377d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/languages/LanguageHelper.java @@ -0,0 +1,61 @@ +/* + * The MIT License + * + * Copyright (c) 2020, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins.scriptsecurity.scripts.languages; + +import hudson.ExtensionList; +import org.jenkinsci.plugins.scriptsecurity.scripts.Language; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; + +@Restricted(NoExternalUse.class) +public final class LanguageHelper { + public static @Nonnull Language getLanguageFromName(@Nonnull String languageName) { + for (Language l : ExtensionList.lookup(Language.class)) { + if (l.getName().equals(languageName)) { + return l; + } + } + return new UnknownLanguage(languageName); + } + + private static class UnknownLanguage extends Language { + private final String languageName; + + private UnknownLanguage(String languageName){ + this.languageName = languageName; + } + + @Override + public String getName() { + return languageName; + } + + @Override + public String getDisplayName() { + return ""; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/FullScriptMetadata.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/FullScriptMetadata.java new file mode 100644 index 000000000..99cec5107 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/FullScriptMetadata.java @@ -0,0 +1,230 @@ +/* + * The MIT License + * + * Copyright (c) 2020, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins.scriptsecurity.scripts.metadata; + +import org.jenkinsci.plugins.scriptsecurity.scripts.ApprovalContext; +import org.jenkinsci.plugins.scriptsecurity.scripts.Language; +import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; +import org.jenkinsci.plugins.scriptsecurity.scripts.languages.LanguageHelper; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashSet; + +@Restricted(NoExternalUse.class) +public class FullScriptMetadata { + + public static final FullScriptMetadata EMPTY = new FullScriptMetadata(true); + + /** + * In characters + */ + private int scriptLength = -1; + private String languageName; + + private int usageCount = 0; + private long lastTimeUsed = -1; + + private long lastApprovalTime = -1; + private boolean wasPreapproved; + + /** + * Only used for the cases where the metadata is missing (legacy or disabled) + */ + private boolean empty = false; + + /** + * When the approver authentication was passed in the context. + * Not necessarily useful for common usage but the difference with {@link #lastApprovalTime} could be valuable for deeper investigation. + */ + private long lastKnownApprovalTime = -1; + private String lastKnownApproverLogin; + + private ApprovalContext lastContext; + private HashSet contextList = new LinkedHashSet<>(); + + public FullScriptMetadata() { + } + + private FullScriptMetadata(boolean empty) { + this.empty = true; + } + + /** + * Called during the configuration of a script + */ + public void notifyApprovalDuringConfiguring(@Nonnull String script, @Nonnull Language language, @Nonnull ApprovalContext context) { + this.updateScriptAndLanguage(script, language.getName()); + + this.updateContext(context); + + long now = new Date().getTime(); + this.lastApprovalTime = now; + + String user = context.getUser(); + if (user != null) { + this.lastKnownApproverLogin = user; + this.lastKnownApprovalTime = now; + } + // as it's approved during the configuration it means that was done by an admin + this.wasPreapproved = true; + } + + /** + * Called when someone/something uses the approved script + */ + public void notifyUsage(@Nonnull String script, @Nonnull Language language) { + this.usageCount++; + this.lastTimeUsed = new Date().getTime(); + + this.updateScriptAndLanguage(script, language.getName()); + } + + /** + * Called when a plugin decides to preapprove a script. + * It's not necessarily a reaction to a user interaction. + */ + public void notifyPreapproveSingle(@Nonnull String script, @Nonnull Language language, @CheckForNull String user) { + long now = new Date().getTime(); + this.lastApprovalTime = now; + + this.updateScriptAndLanguage(script, language.getName()); + + if (user != null) { + this.lastKnownApproverLogin = user; + this.lastKnownApprovalTime = now; + } + + this.wasPreapproved = true; + } + + /** + * Called when a plugin decides to preapprove a script. + * It's not necessarily a reaction to a user interaction. + * + * Expected to come only from test code, not production code. + */ + public void notifyPreapproveAll(@Nonnull ScriptApproval.PendingScript pendingScript, @CheckForNull String approverLogin) { + long now = new Date().getTime(); + this.lastApprovalTime = now; + + this.updateScriptAndLanguage(pendingScript.script, pendingScript.getLanguageName()); + + ApprovalContext context = pendingScript.getContext(); + this.updateContext(context); + + if (approverLogin != null) { + this.lastKnownApproverLogin = approverLogin; + this.lastKnownApprovalTime = now; + } + + this.wasPreapproved = true; + } + + /** + * Called when a script was approved from the ScriptSecurity page + */ + public void notifyApproval(@CheckForNull ScriptApproval.PendingScript pendingScript, @CheckForNull String approverLogin) { + long now = new Date().getTime(); + if (pendingScript != null) { + this.updateScriptAndLanguage(pendingScript.script, pendingScript.getLanguageName()); + + ApprovalContext context = pendingScript.getContext(); + this.updateContext(context); + + if (approverLogin != null) { + this.lastKnownApproverLogin = approverLogin; + this.lastKnownApprovalTime = now; + } + } + this.lastApprovalTime = now; + } + + private void updateScriptAndLanguage(@Nonnull String script, @Nonnull String languageName) { + this.scriptLength = script.length(); + this.languageName = languageName; + } + + private void updateContext(@Nonnull ApprovalContext context) { + this.contextList.add(context); + this.lastContext = context; + } + + public boolean isEmpty() { + return empty; + } + + public int getUsageCount() { + return usageCount; + } + + public @CheckForNull Date getLastTimeUsedDate() { + if (lastTimeUsed == -1) { + return null; + } + return new Date(lastTimeUsed); + } + + public long getLastTimeUsed() { + return lastTimeUsed; + } + + public boolean isWasPreapproved() { + return wasPreapproved; + } + + public @CheckForNull Date getLastApprovalTimeDate() { + if (lastApprovalTime == -1) { + return null; + } + return new Date(lastApprovalTime); + } + + public long getLastApprovalTime() { + return lastApprovalTime; + } + + public @CheckForNull String getLastKnownApproverLogin() { + return lastKnownApproverLogin; + } + + public @CheckForNull Language getLanguage() { + if (languageName == null) { + return null; + } + return LanguageHelper.getLanguageFromName(languageName); + } + + public @CheckForNull ApprovalContext getLastContext() { + return lastContext; + } + + public int getScriptLength() { + return scriptLength; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/HashAndFullScriptMetadata.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/HashAndFullScriptMetadata.java new file mode 100644 index 000000000..ec8a803ae --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/HashAndFullScriptMetadata.java @@ -0,0 +1,40 @@ +/* + * The MIT License + * + * Copyright (c) 2020, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins.scriptsecurity.scripts.metadata; + +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.Nonnull; + +@Restricted(NoExternalUse.class) +public class HashAndFullScriptMetadata { + public final String hash; + public final FullScriptMetadata metadata; + + public HashAndFullScriptMetadata(@Nonnull String hash, @Nonnull FullScriptMetadata metadata) { + this.hash = hash; + this.metadata = metadata; + } +} diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java new file mode 100644 index 000000000..f14ea5bee --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java @@ -0,0 +1,232 @@ +/* + * The MIT License + * + * Copyright (c) 2020, CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.jenkinsci.plugins.scriptsecurity.scripts.metadata; + +import hudson.XmlFile; +import jenkins.model.Jenkins; +import org.apache.commons.io.FileUtils; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +@Restricted(NoExternalUse.class) +public class MetadataStorage { + private static final Logger LOGGER = Logger.getLogger(MetadataStorage.class.getName()); + /** + * Currently only lowercase hex characters are necessary but we will need additional characters + * When we will support SHA-256 or other, in order to support migration. + * + * This prevents path traversal attempts + */ + private static final Pattern HASH_REGEX = Pattern.compile("[a-zA-Z0-9_\\-]+"); + private static final String METADATA_FILE_NAME = "metadata.xml"; + private static final String SCRIPT_FILE_NAME = "script.txt"; + + private final File metadataFolder; + + /** + * Metadata about full scripts. + * They can exist before approval and remains after revocation. + */ + private HashMap hashToMetadata; + + public MetadataStorage(@Nonnull String metadataFolderPath) { + this.metadataFolder = new File(Jenkins.getInstance().getRootDir(), metadataFolderPath); + if (metadataFolder.mkdirs()) { + LOGGER.log(Level.FINER, "Metadata storage folder created: {0}", metadataFolder.getAbsolutePath()); + } + } + + public @Nonnull List getMetadataUsingHashes(@Nonnull Collection approvedScriptHashes) { + this.ensureLoaded(); + List result = new ArrayList<>(approvedScriptHashes.size()); + + for (String hash : approvedScriptHashes) { + FullScriptMetadata metadata = hashToMetadata.getOrDefault(hash, FullScriptMetadata.EMPTY); + HashAndFullScriptMetadata hashAndMeta = new HashAndFullScriptMetadata(hash, metadata); + result.add(hashAndMeta); + } + return result; + } + + /** + * Lazy loading to avoid slowing down the startup of the instance + */ + private void ensureLoaded() { + if (hashToMetadata != null) { + return; + } + + LOGGER.log(Level.INFO, "Loading script approval metadata..."); + long startTime = System.currentTimeMillis(); + + loadAllMetadata(); + + long endTime = System.currentTimeMillis(); + LOGGER.log(Level.INFO, "All metadata loaded in {0} ms", endTime - startTime); + } + + /** + * This method could be called by Script Console to reload the metadata + * + * org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.get().metadataStorage.loadAllMetadata() + */ + private void loadAllMetadata() { + hashToMetadata = new HashMap<>(); + String[] hashes = metadataFolder.list(); + for (int i = 0; i < hashes.length; i++) { + String hash = hashes[i]; + File metadataFile = new File(metadataFolder, hash + File.separator + METADATA_FILE_NAME); + if (metadataFile.exists()) { + XmlFile xmlFile = new XmlFile(metadataFile); + try { + Object content = xmlFile.read(); + if (content instanceof FullScriptMetadata) { + FullScriptMetadata metadata = (FullScriptMetadata) content; + this.hashToMetadata.put(hash, metadata); + } else { + LOGGER.log(Level.WARNING, "Invalid class read for the metadata for hash {0}, found: {1}", new Object[]{hash, content.getClass()}); + } + } catch (IOException e) { + LOGGER.log(Level.INFO, "Impossible to read the metadata for hash {0}.", hash); + } + } + } + } + + public @CheckForNull FullScriptMetadata getExisting(@Nonnull String hash) { + ensureLoaded(); + return hashToMetadata.get(hash); + } + + public void withMetadata(@Nonnull String hash, @CheckForNull String script, @Nonnull Consumer consumer) { + ensureLoaded(); + ensureValidHash(hash); + + FullScriptMetadata metadata = hashToMetadata.get(hash); + if (metadata == null) { + metadata = new FullScriptMetadata(); + hashToMetadata.put(hash, metadata); + } + + consumer.accept(metadata); + + saveMetadata(hash, metadata); + if (script != null) { + saveScript(hash, script); + } + } + + public void removeHash(@Nonnull String hash) { + ensureLoaded(); + ensureValidHash(hash); + FullScriptMetadata metadata = hashToMetadata.remove(hash); + if (metadata == null) { + return; + } + + deleteHashFolder(hash); + } + + private void saveMetadata(@Nonnull String hash, @Nonnull FullScriptMetadata metadata) { + File hashFolder = new File(metadataFolder, hash); + if (hashFolder.mkdirs()) { + LOGGER.log(Level.FINER, "Metadata folder created for hash {0}", hash); + } + + XmlFile xmlFile = new XmlFile(new File(hashFolder, METADATA_FILE_NAME)); + try { + xmlFile.write(metadata); + } catch (IOException e) { + LOGGER.log(Level.WARNING, e, () -> "Failed to save the metadata for hash: " + hash); + } + } + + private void deleteHashFolder(@Nonnull String hash) { + File targetFolder = new File(metadataFolder, hash); + + try { + boolean existed = targetFolder.exists(); + FileUtils.deleteDirectory(targetFolder); + if (existed) { + LOGGER.log(Level.FINER, "Metadata related to {0} removed", hash); + } else { + LOGGER.log(Level.FINER, "Metadata related to {0} did not exist", hash); + } + } catch (IOException e) { + LOGGER.log(Level.FINE, "Impossible to delete the metadata folder found for {0}", hash); + } + } + + private void ensureValidHash(@Nonnull String hash) { + if (!HASH_REGEX.matcher(hash).matches()) { + throw new IllegalArgumentException("The provided hash is invalid: " + hash); + } + } + + /** + * Assume the {@link #saveMetadata(String, FullScriptMetadata)} is called first to create the parent folder + */ + private void saveScript(@Nonnull String hash, @Nonnull String script) { + File file = new File(metadataFolder, hash + File.separator + SCRIPT_FILE_NAME); + try { + FileUtils.write(file, script, StandardCharsets.UTF_8); + } catch (IOException e) { + LOGGER.log(Level.WARNING, e, () -> "Failed to save the script for hash: " + hash); + } + } + + /** + * Read the script if the file exists + */ + public @CheckForNull String readScript(@Nonnull String hash) { + ensureLoaded(); + ensureValidHash(hash); + + File file = new File(metadataFolder, hash + File.separator + SCRIPT_FILE_NAME); + if (!file.exists()) { + return null; + } + try { + String script = FileUtils.readFileToString(file, StandardCharsets.UTF_8); + return script; + } catch (IOException e) { + LOGGER.log(Level.WARNING, e, () -> "Failed to save the script for hash: " + hash); + } + return null; + } +} diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties index 522fb4e9e..802f0333d 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties @@ -2,3 +2,10 @@ ClasspathEntry.path.notExists=Specified path does not exist ClasspathEntry.path.notApproved=This classpath entry is not approved. Require an approval before execution. ClasspathEntry.path.noDirsAllowed=Class directories are not allowed as classpath entries. ScriptApprovalNote.message=Administrators can decide whether to approve or reject this signature. + +ScriptApproval.tab.fullScriptPending=Script - pending approvals +ScriptApproval.tab.fullScriptApproved=Script - approved +ScriptApproval.tab.signaturePending=Signature - pending approvals +ScriptApproval.tab.signatureApproved=Signature - approved +ScriptApproval.tab.classPathPending=Class path - pending approvals +ScriptApproval.tab.classPathApproved=Class path - approved diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css new file mode 100644 index 000000000..4f81f9cf4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css @@ -0,0 +1,113 @@ +/* TODO using a plugin-specific style until the core contains such style and this plugin depends on it */ +/* copied from Bootstrap 4 style */ +.script-approval-page .custom-badge { + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25rem; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; +} + +.script-approval-page .custom-badge-primary { + color: #fff; + background-color: #007bff; +} + +.script-approval-page .custom-badge-standard { + color: #fff; + background-color: #6c757d; +} + +.script-approval-page .custom-pane-frame-padding { + padding: 8px 12px; + + /* border part stolen from pane-frame */ + border: solid 1px #f0f0f0; + border-radius: 4px; +} + +.script-approval-page h2 ~ h2 { + margin-top: 35px; +} + +.script-approval-page table th { + text-align: left; +} + +.script-approval-page tr.selected { + background-color: #f9f8de; +} + +.script-approval-page td.hash { + font-family: monospace; +} + +.script-approval-page tr td.hidden-by-default { + display: none; +} +.script-approval-page .toggle-parent { + display: inline-block; + font-size: 14px; + font-weight: normal; +} + +.script-approval-page .hidden-element { + display: none; +} + +/* + Hack over the Jenkins textarea.js that computes the textarea height to force CodeMirror + but it has a different line-height (at least) and thus, the height becomes too big for it. +*/ +.script-approval-page textarea { + font-family: monospace; + font-size: 14px; + line-height: 14px; +} + +.script-approval-page .no-pending-script-approvals, +.script-approval-page .no-approved-scripts { + margin: 12px; + font-style: italic; +} + +.script-approval-page .info-cursor { + cursor: help; +} + +.script-approval-page .action-panel { + margin-top: 12px; +} + +.script-approval-page .action-panel button { + margin-top: 5px; + margin-right: 8px; +} + +.script-approval-page .action-link { + text-decoration: none; + color: #204A87; + cursor: pointer; +} + +.script-approval-page .action-link:hover, +.script-approval-page .action-link:focus { + text-decoration: underline; +} + +.script-approval-page .action-link:visited { + /* avoid visited behavior */ + color: #204A87; +} +.script-approval-page .add-margin { + margin: 5px; +} + +.script-approval-page .space-before { + margin-top: 12px; +} diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.js b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.js new file mode 100644 index 000000000..dbc41488d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.js @@ -0,0 +1,215 @@ +(function() { + function toggleTargetDisplay(element, isShowDesired) { + var parentToggle = element.parentElement; + var targetId = parentToggle.getAttribute('data-expand-target-id'); + var isCodeMirror = 'true' === parentToggle.getAttribute('data-expand-codemirror'); + var isAsyncCodeMirror = 'true' === parentToggle.getAttribute('data-expand-async-codemirror'); + var target = document.getElementById(targetId); + if (!target) { + console.warn('No target found for id', targetId); + } + + var showButton = parentToggle.querySelector('.js-toggle-show-icon'); + var hideButton = parentToggle.querySelector('.js-toggle-hide-icon'); + if (isShowDesired) { + target.classList.remove('hidden-element'); + showButton.classList.add('hidden-element'); + hideButton.classList.remove('hidden-element'); + } else { + target.classList.add('hidden-element'); + hideButton.classList.add('hidden-element'); + showButton.classList.remove('hidden-element'); + } + + if (isCodeMirror) { + var textArea = target.querySelector('textarea'); + var cm = textArea.codemirrorObject; + if (cm) { + // checking existence first for languages not supported by CodeMirror (like System Commands) + // refresh prevents a buggy scrollbar appearance due to dynamical render + cm.refresh(); + } + } else if (isShowDesired && isAsyncCodeMirror) { + var asyncUrl = parentToggle.getAttribute('data-expand-url'); + + var loadingElement = target.querySelector('.js-expand-async-loading'); + var errorElementOther = target.querySelector('.js-expand-async-error'); + var errorElement403 = target.querySelector('.js-expand-async-error-403'); + // reset status first + if (loadingElement) { + loadingElement.classList.remove('hidden-element'); + } + if (errorElementOther) { + errorElementOther.classList.add('hidden-element'); + } + if (errorElement403) { + errorElement403.classList.add('hidden-element'); + } + + // no need to re-load the script the next time + parentToggle.setAttribute('data-expand-async-codemirror', 'already-loaded'); + + new Ajax.Request(asyncUrl, { + contentType:"application/json", + encoding:"UTF-8", + onSuccess: function(rsp) { + if (loadingElement) { + loadingElement.classList.add('hidden-element'); + } + + var scriptContent = rsp.responseText; + + // scriptContent is escaped inside _scriptContent.jelly + target.innerHTML = scriptContent; + + // from hudson-behavior.js + evalInnerHtmlScripts(scriptContent, function() { + Behaviour.applySubtree(target); + var textArea = target.querySelector('textarea'); + var cm = textArea.codemirrorObject; + if (cm) { + // checking existence first for languages not supported by CodeMirror (like System Commands) + // refresh prevents a buggy scrollbar appearance due to dynamical render + cm.refresh(); + } + // next expansion the code has to be refreshed + parentToggle.setAttribute('data-expand-codemirror', 'true'); + }); + }, + onFailure: function(response) { + if (loadingElement) { + loadingElement.classList.add('hidden-element'); + } + if (response.status === 403) { + // most likely to be a CSRF issue due to disconnection + if (errorElement403) { + errorElement403.classList.remove('hidden-element'); + } + } else { + // 404 or otherthing, generic message + if (errorElementOther) { + errorElementOther.classList.remove('hidden-element'); + } + } + } + }); + } + } + + Behaviour.specify(".js-toggle-show-icon", "show-icon", 0, function(element) { + element.observe('click', function() { + toggleTargetDisplay(element, true); + }); + }); + + Behaviour.specify(".js-toggle-hide-icon", "hide-icon", 0, function(element) { + element.observe('click', function() { + toggleTargetDisplay(element, false); + }); + }); + + // the priority ensures it's executed after "TEXTAREA.codemirror" + Behaviour.specify(".js-async-hidden-element", "replace-js-async", /* priority */ 1, function(element) { + element.classList.add('hidden-element'); + element.classList.remove('js-async-hidden-element'); + }); + + Behaviour.specify("table tr.js-selectable-row", "click-to-select", 0, function(row) { + row.observe('click', onLineClicked); + }); + + Behaviour.specify("table tr.js-selectable-row input[type='checkbox']", "click-to-select", 0, function(checkbox) { + checkbox.observe('change', function() { onCheckChanged(this) }); + }); + + function onLineClicked(event){ + var line = this; + // to allow click on checkbox or on label to act normally + var targetBypassClick = event.target && event.target.classList.contains('js-bypass-click'); + if (targetBypassClick) { + return; + } + + var checkbox = line.querySelector('input[type="checkbox"]'); + checkbox.checked = !checkbox.checked; + onCheckChanged(checkbox); + } + + function onCheckChanged(checkBox){ + // up is a prototype helper method + var line = checkBox.up('tr'); + if (checkBox.checked) { + line.addClassName('selected'); + } else { + line.removeClassName('selected'); + } + } + + // ######### For fullScript_pending.jelly ######### + + Behaviour.specify(".js-button-pending-approve-all", "approved-approve-all", 0, function(element) { + element.observe('click', handleClickToProcessAction); + }); + + Behaviour.specify(".js-button-pending-deny-all", "approved-deny-all", 0, function(element) { + element.observe('click', handleClickToProcessAction); + }); + + // ######### For fullScript_approved.jelly ######### + + Behaviour.specify(".js-button-approved-revoke-all", "approved-revoke-all", 0, function(element) { + element.observe('click', handleClickToProcessAction); + }); + + function handleClickToProcessAction() { + var element = this; + var containerId = element.getAttribute('data-container-id'); + var actionUrl = element.getAttribute('data-action-url'); + var confirmMessage = element.getAttribute('data-action-confirm-message'); + + processAction(containerId, actionUrl, confirmMessage); + } + + function processAction(containerId, actionUrl, confirmMessage) { + var container = document.getElementById(containerId); + var allCheckboxes = container.querySelectorAll('input[type="checkbox"].js-checkbox-hash'); + var checkedHashes = []; + allCheckboxes.forEach(function(c){ + if (c.checked) { + var hash = c.getAttribute('data-hash'); + checkedHashes.push(hash); + } + }); + + var errorPanel = container.querySelector('.js-error-no-selected'); + if (checkedHashes.length === 0) { + if (errorPanel) { + errorPanel.classList.remove('hidden-element'); + } else { + console.warn('There is no selected item, cannot proceed with the action.'); + } + return; + } else { + if (errorPanel) { + errorPanel.classList.add('hidden-element'); + } + } + + if (confirmMessage) { + var wasConfirmed = confirm(confirmMessage); + if (!wasConfirmed) { + return; + } + } + + var params = {hashes: checkedHashes}; + new Ajax.Request(actionUrl, { + postBody: Object.toJSON(params), + contentType:"application/json", + encoding:"UTF-8", + onComplete: function(rsp) { + window.location.reload(); + } + }); + } +})(); \ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly index 6a9ed46b9..9b92ad788 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.jelly @@ -23,252 +23,22 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --> - + - + + - - - -

- No pending script approvals. -

- - - -
-

- / ${ps.language.displayName} script - : -

- -
+
+ + + - - -

- You can also remove all previous script approvals: - -

-
- - -

- No pending signature approvals. -

-
- - -
-

- / - - / - - signature - : - ${s.signature} - - Approving this signature may introduce a security vulnerability! You are advised to deny it. - -

-
-
-
-
-

Signatures already approved:

- -

Signatures already approved assuming permission check:

- - - -

Signatures already approved which may have introduced a security vulnerability (recommend clearing):

- -
-

- You can also remove all previous signature approvals: - -

- - Or you can just remove the dangerous ones: - - -
-

- No pending classpath entry approvals. -

-
- Classpath entries pending approval. (Beware of remote URLs, workspace files, or anything else that might change without your notice.) -
-

Classpath entries already approved:

-

- No approved classpath entries. -

-
+ +
-

- You can also remove all previous classpath entry approvals: - -

diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties new file mode 100644 index 000000000..d157bcb43 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties @@ -0,0 +1,3 @@ +fullScriptApproval=Full script approvals +signatureApproval=Signature approvals +classPathApproval=Classpath entry approvals diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly new file mode 100644 index 000000000..40ce26803 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly @@ -0,0 +1,57 @@ + + + + + + + + + The name of the tab + + + The url of the tab + + + Whether the tab is active or not + + + The title of the tab + + + The number of notification to be displayed as a badge inside the tab name + + + Determine whether the color of the badge should be the primary color or a less highlighted one + + + + ${attrs.name} + + + ${attrs.numOfNotification} + + + diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.properties new file mode 100644 index 000000000..d8eabe8c4 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.properties @@ -0,0 +1 @@ +numberOfPendingEntries=Number of pending entries diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly new file mode 100644 index 000000000..d03719ec2 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly @@ -0,0 +1,54 @@ + + + + + + + + + The url of the tab + + + Whether the tab is active or not + + + The title of the tab + + +
+ + + + + + + + + + + +
+ +
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/_scriptContent.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/_scriptContent.jelly new file mode 100644 index 000000000..0c3d146cf --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/_scriptContent.jelly @@ -0,0 +1,10 @@ + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly new file mode 100644 index 000000000..5f515ad9f --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly @@ -0,0 +1,75 @@ + + +
+

Classpath entries already approved:

+

+ No approved classpath entries. +

+
+
+

+ You can also remove all previous classpath entry approvals: + +

+
+ + +
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly new file mode 100644 index 000000000..75d02d9be --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly @@ -0,0 +1,74 @@ + + +
+

+ No pending classpath entry approvals. +

+
+ Classpath entries pending approval. (Beware of remote URLs, workspace files, or anything else that might change without your notice.) +
+
+ + +
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly new file mode 100644 index 000000000..70e499864 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly @@ -0,0 +1,204 @@ + + +
+ +

+ ${%approvedScripts} +

+ +
+ +
+ ${%metadataGatheringDisabled} +
+
+ + +

+ ${%noApprovedScripts} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
${%approvedHash}${%approvedExpandCode}${%approvedLanguage}${%approvedScriptLength}${%approvedContextUser}${%approvedContextItem}${%usageCount}${%lastTimeUsed} + + ${%lastApprovalTime}${%lastKnownApproverLogin}
+ + + + + ${%emptyMetadata} + + + +
+ ${%approvedExpand} + ${%approvedMinimize} +
+
+ + + ${%noScriptCodeSinceMetadata} + + +
+
${language.displayName}${metadata.scriptLength} + + + + ${user} + + + + ${%noRequesterSinceMetadata} + + + + + + + + ${contextItem.fullDisplayName} + + + + ${%noContextItemSinceMetadata} + + + + + + + + ${usageCount} + + + + ${%noUsageCountSinceMetadata} + + + + + + + + + + + + ${%notUsedSinceMetadata} + + + + + + + + + + + + + + + + + + ${%notApprovedSinceMetadata} + + + + + + + + ${lastKnownApproverLogin} + + + + ${%noKnownApproverSinceMetadata} + + + +
+
+ ${%scriptContentLoadingMessage} +
+
+ ${%scriptContentLoadError} +
+
+ ${%scriptContentLoadError403} +
+
+ +

+ ${%approvedNoSelectedItemError} +

+ +
+ + + +
+
+
+
+
+
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties new file mode 100644 index 000000000..2575bf06d --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties @@ -0,0 +1,63 @@ +approvedScripts=Approved scripts + +# approved scripts +metadataGatheringDisabled=The metadata gathering was disabled. \ +

You can re-enable it permanently by removing the system property:
\ + org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.metadataGathering

\ +

or temporarily (until restart) by running this in the Script Console:
\ + org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.METADATA_GATHERING = true

+ +## table headers +approvedHash=Hash +approvedHash_tooltip=The hash could be seen as a signature of the script. It's used as an identifier to reference the content of the script. +approvedExpandCode=Code +approvedLanguage=Language +approvedScriptLength=Length +approvedScriptLength_tooltip=Length of the script in characters +usageCount=# of uses +usageCount_tooltip=Usage since the introduction of metadata +lastTimeUsed=Date of last use +wasPreapproved_header_tooltip=The presence of a green tick means the script was approved during the configuration. \ + Otherwise it was created by a user without the permission to run scripts and was then approved using this page. +lastApprovalTime=Date of the last approval +lastApprovalTime_tooltip=A single script can be approved multiple times. It occurs every time the related configuration is saved. +lastKnownApproverLogin=Last approver + +## table content +noRequesterSinceMetadata=N/A +noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script does not provide the user information. +noRequestApprovalTimeSinceMetadata=N/A +noRequestApprovalTimeSinceMetadata_tooltip=It was requested before the introduction of metadata. +noContextItemSinceMetadata=N/A +noContextItemSinceMetadata_tooltip=The context item was not provided by the code asking for the approval. + +emptyMetadata=There is no metadata for this approval +emptyMetadata_tooltip=If the script is used after the metadata introduction, some metadata will be displayed here. \ + It means that an unused approval will not have metadata and could be reasonably revoked after a certain period of time. +noScriptCodeSinceMetadata=N/A +noScriptCodeSinceMetadata_tooltip=The approval was given while the metadata were not gathered. +approvedContextUser=Requester +approvedContextItem=Context +wasPreapproved_tooltip=The script content was approved directly during configuration, meaning it comes from a user with the permission to run scripts. +wasPreapproved_alt=Preapproved +noUsageCountSinceMetadata=N/A +noUsageCountSinceMetadata_tooltip=It was not used since the introduction of metadata. +notUsedSinceMetadata=N/A +notUsedSinceMetadata_tooltip=It was not used since the introduction of metadata. +notApprovedSinceMetadata=N/A +notApprovedSinceMetadata_tooltip=It was not approved since the introduction of metadata. +noKnownApproverSinceMetadata=N/A +noKnownApproverSinceMetadata_tooltip=It was not approved since the introduction of metadata. + +revokeApproval=Revoke all selected +revokeApproval_tooltip=Revoking an approved script can have an impact on the jobs using it. The next time an attempt to execute the script is made, a new approval will be required. +revokeApproval_confirm=Really delete all selected approvals? Any existing scripts will need to be requeued and reapproved. +approvedNoSelectedItemError=Please select at least one line. + +approvedExpand=expand code +approvedExpand_tooltip=Expand the source code visualization +approvedMinimize=minimize code +approvedMinimize_tooltip=Minimize the source code visualization +scriptContentLoadingMessage=Loading... +scriptContentLoadError=Impossible to find the script content. It was perhaps removed manually. On next usage this should be self-repaired. +scriptContentLoadError403=Impossible to request the script content due to lack of authorization. Try reloading the page. diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly new file mode 100644 index 000000000..5796cabc3 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly @@ -0,0 +1,117 @@ + + +
+ + +

+ ${%pendingScripts} +

+
+ + +

+ ${%noPendingScripts} +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
${%pendingHash}${%pendingExpandCode}${%pendingLanguage}${%pendingScriptLength}${%contextUser}${%contextItem}${%requestApprovalTime}
+ + + + +
+ ${%pendingExpand} + ${%pendingMinimize} +
+
${ps.language.displayName}${ps.script.size()} + + + + ${user} + + + + ${%noRequesterSinceMetadata} + + + + + + + + ${contextItem.fullDisplayName} + + + + ${%noContextItemSinceMetadata} + + + + + + + + + + + + ${%noRequestApprovalTimeSinceMetadata} + + + +
+ +
+ +

+ ${%pendingNoSelectedItemError} +

+ +
+ + + + + + +
+
+
+
+
+
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties new file mode 100644 index 000000000..46084b504 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties @@ -0,0 +1,38 @@ +pendingScripts=Scripts pending approval + +# pending scripts +## table headers +pendingHash=Hash +pendingHash_tooltip=The hash could be seen as a signature of the script. It's used as an identifier to reference the content of the script. +pendingExpandCode=Code +pendingLanguage=Language +pendingScriptLength=Length +pendingScriptLength_tooltip=Length of the script in characters +contextUser=Requester +contextItem=Context +requestApprovalTime=Request date +requestApprovalTime_tooltip=Inform about when the approval request was done. + +## table content +noRequesterSinceMetadata=N/A +noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script does not provide the user information. +noRequestApprovalTimeSinceMetadata=N/A +noRequestApprovalTimeSinceMetadata_tooltip=It was requested before the introduction of metadata. +noContextItemSinceMetadata=N/A +noContextItemSinceMetadata_tooltip=The context item was not provided by the code asking for the approval. + +approvePending=Approve all selected +approvePending_tooltip=Approving a pending script will allow any user to execute it. You need to be careful to only approve non-dangerous scripts. +approvePending_confirm=Really approve all selected scripts? Have you checked that none of them are dangerous? +denyPending=Deny all selected +denyPending_tooltip=Denying a pending script will just remove it from the pending list. If there are new script execution attempts, it will reappear in this list. +denyPending_confirm=Really deny all selected scripts? If they are triggered again, they will reappear in this list. +pendingNoSelectedItemError=Please select at least one line. + +pendingExpand=expand code +pendingExpand_tooltip=Expand the source code visualization +pendingMinimize=minimize code +pendingMinimize_tooltip=Minimize the source code visualization + +noPendingScripts=No pending script approvals. +noApprovedScripts=No approved scripts. diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly new file mode 100644 index 000000000..c3c2ba1f3 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly @@ -0,0 +1,70 @@ + + +
+
Signatures already approved:
+ + +
Signatures already approved assuming permission check:
+ + + + +
Signatures already approved which may have introduced a security vulnerability (recommend clearing):
+ +
+

+ You can also remove all previous signature approvals: + +

+ + Or you can just remove the dangerous ones: + + +
+ + +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly new file mode 100644 index 000000000..9eb037e11 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly @@ -0,0 +1,70 @@ + + +
+ + +

+ No pending signature approvals. +

+
+ + +
+

+ / + + / + + signature + : + ${s.signature} + + Approving this signature may introduce a security vulnerability! You are advised to deny it. + +

+
+
+
+
+
+ + +
\ No newline at end of file diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/taglib b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/taglib new file mode 100644 index 000000000..e69de29bb From b7a3312aa5cbc6f4431128bb30e8adad1f0e3d99 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:15:36 +0200 Subject: [PATCH 02/21] Update src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java Co-authored-by: Josh Soref --- .../plugins/scriptsecurity/scripts/ScriptApproval.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java index 60be34ef9..f107419f4 100644 --- a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java @@ -999,7 +999,7 @@ public void doScriptContent(StaplerRequest req, StaplerResponse rsp, @QueryParam synchronized (this) { FullScriptMetadata metadata = this.metadataStorage.getExisting(hash); if (metadata == null) { - // this can occur naturally only if you have concurrent view on the scriptApproval page + // this can occur naturally only if you have concurrent views of the scriptApproval page LOG.log(Level.FINER, "Requesting a non-existing metadata {0}", hash); throw HttpResponses.notFound(); } From 4c583a47e374e8c5be336c9d638e44446e29969a Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:15:47 +0200 Subject: [PATCH 03/21] Update src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java Co-authored-by: Josh Soref --- .../plugins/scriptsecurity/scripts/ScriptApproval.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java index f107419f4..d4303690f 100644 --- a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval.java @@ -1293,7 +1293,7 @@ public synchronized JSON clearApprovedClasspathEntries() throws IOException { } /** - * To have effectively final variable + * To have an effectively final variable */ private @CheckForNull String getCurrentUserLogin() { String userLogin = null; From 349ac0493eb7138e2d02ee25aec9af79f99bd78e Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:16:43 +0200 Subject: [PATCH 04/21] Update src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java Co-authored-by: Josh Soref --- .../scriptsecurity/scripts/metadata/MetadataStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java index f14ea5bee..3adfccbf0 100644 --- a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java @@ -48,7 +48,7 @@ public class MetadataStorage { private static final Logger LOGGER = Logger.getLogger(MetadataStorage.class.getName()); /** * Currently only lowercase hex characters are necessary but we will need additional characters - * When we will support SHA-256 or other, in order to support migration. + * to support SHA-256 or other, in order to support migration. * * This prevents path traversal attempts */ From c559fe8c58b320dd31c33ad074cddd748f561173 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:16:59 +0200 Subject: [PATCH 05/21] Update src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java Co-authored-by: Josh Soref --- .../scriptsecurity/scripts/metadata/MetadataStorage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java index 3adfccbf0..4135ba97e 100644 --- a/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java +++ b/src/main/java/org/jenkinsci/plugins/scriptsecurity/scripts/metadata/MetadataStorage.java @@ -60,7 +60,7 @@ public class MetadataStorage { /** * Metadata about full scripts. - * They can exist before approval and remains after revocation. + * They can exist before approval and remain after revocation. */ private HashMap hashToMetadata; From 1d39eff304da40c8764c23f72168e44a314b81ef Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:18:19 +0200 Subject: [PATCH 06/21] Update src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties Co-authored-by: Josh Soref --- .../scriptsecurity/scripts/Messages.properties | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties index 802f0333d..698a24e72 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties @@ -3,9 +3,9 @@ ClasspathEntry.path.notApproved=This classpath entry is not approved. Require an ClasspathEntry.path.noDirsAllowed=Class directories are not allowed as classpath entries. ScriptApprovalNote.message=Administrators can decide whether to approve or reject this signature. -ScriptApproval.tab.fullScriptPending=Script - pending approvals -ScriptApproval.tab.fullScriptApproved=Script - approved -ScriptApproval.tab.signaturePending=Signature - pending approvals -ScriptApproval.tab.signatureApproved=Signature - approved -ScriptApproval.tab.classPathPending=Class path - pending approvals -ScriptApproval.tab.classPathApproved=Class path - approved +ScriptApproval.tab.fullScriptPending=Scripts - pending approval +ScriptApproval.tab.fullScriptApproved=Scripts - approved +ScriptApproval.tab.signaturePending=Signatures - pending approval +ScriptApproval.tab.signatureApproved=Signatures - approved +ScriptApproval.tab.classPathPending=Class paths - pending approval +ScriptApproval.tab.classPathApproved=Class paths - approved From de369d69d86e0d08ea1a3ec10cb358430fca5605 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:18:38 +0200 Subject: [PATCH 07/21] Update src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly Co-authored-by: Josh Soref --- .../scriptsecurity/scripts/ScriptApproval/tab-custom.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly index 40ce26803..670cacacf 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly @@ -35,7 +35,7 @@ THE SOFTWARE. The url of the tab - Whether the tab is active or not + Whether or not the tab is active The title of the tab From e4708f7f2d9270a4b71f692fa5895c41db94d798 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:19:02 +0200 Subject: [PATCH 08/21] Update src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties Co-authored-by: Josh Soref --- .../scripts/ScriptApproval/tabs/fullScript_pending.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties index 46084b504..93744219c 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties @@ -34,5 +34,5 @@ pendingExpand_tooltip=Expand the source code visualization pendingMinimize=minimize code pendingMinimize_tooltip=Minimize the source code visualization -noPendingScripts=No pending script approvals. +noPendingScripts=No scripts pending approval. noApprovedScripts=No approved scripts. From 380deda3954f358d8673129dee3ebbcda32d7ca2 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:20:01 +0200 Subject: [PATCH 09/21] Update src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly Co-authored-by: Josh Soref --- .../scripts/ScriptApproval/tabs/signature_approved.jelly | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly index c3c2ba1f3..ea52a11d2 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly @@ -13,7 +13,7 @@ -
Signatures already approved which may have introduced a security vulnerability (recommend clearing):
+
Signatures already approved which may have introduced security vulnerabilities (recommend clearing):
@@ -67,4 +67,4 @@ }); } - \ No newline at end of file + From 79b1995c7226bc8664c669ae82475e9989cae419 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:20:53 +0200 Subject: [PATCH 10/21] Update src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties Co-authored-by: Josh Soref --- .../scripts/ScriptApproval/tabs/fullScript_pending.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties index 93744219c..4b141aa0b 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties @@ -15,7 +15,7 @@ requestApprovalTime_tooltip=Inform about when the approval request was done. ## table content noRequesterSinceMetadata=N/A -noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script does not provide the user information. +noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script did not provide user information. noRequestApprovalTimeSinceMetadata=N/A noRequestApprovalTimeSinceMetadata_tooltip=It was requested before the introduction of metadata. noContextItemSinceMetadata=N/A From 0ea0c93a85ec59af5023c8d15ef974ad3cebeaf6 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:21:04 +0200 Subject: [PATCH 11/21] Update src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties Co-authored-by: Josh Soref --- .../scripts/ScriptApproval/tabs/fullScript_pending.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties index 4b141aa0b..ab56d598a 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties @@ -11,7 +11,7 @@ pendingScriptLength_tooltip=Length of the script in characters contextUser=Requester contextItem=Context requestApprovalTime=Request date -requestApprovalTime_tooltip=Inform about when the approval request was done. +requestApprovalTime_tooltip=Inform about when the approval request was made. ## table content noRequesterSinceMetadata=N/A From e9785594bff492fb4800a3de01d67182f3e0325f Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:21:44 +0200 Subject: [PATCH 12/21] Update src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly Co-authored-by: Josh Soref --- .../scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly index d03719ec2..d1fed2919 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-with-body.jelly @@ -31,7 +31,7 @@ THE SOFTWARE. The url of the tab
- Whether the tab is active or not + Whether or not the tab is active The title of the tab From eadf75f1a0ce3c05b53db3b4452695155fdc58ee Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Fri, 29 May 2020 15:50:31 +0200 Subject: [PATCH 13/21] Apply suggestions from code review Co-authored-by: Josh Soref --- .../scripts/ScriptApproval/tab-custom.jelly | 2 +- .../ScriptApproval/tabs/classPath_pending.jelly | 2 +- .../tabs/fullScript_approved.properties | 12 ++++++------ .../ScriptApproval/tabs/signature_pending.jelly | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly index 670cacacf..ad07f81cd 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tab-custom.jelly @@ -41,7 +41,7 @@ THE SOFTWARE. The title of the tab - The number of notification to be displayed as a badge inside the tab name + The number, of notifications, to be displayed as a badge inside the tab name Determine whether the color of the badge should be the primary color or a less highlighted one diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly index 75d02d9be..55e09dc9c 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly @@ -5,7 +5,7 @@ No pending classpath entry approvals.

- Classpath entries pending approval. (Beware of remote URLs, workspace files, or anything else that might change without your notice.) + Classpath entries pending approval. (Beware of remote URLs, workspace files, or anything else that might change without you noticing.)
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties index 2575bf06d..62cfc7627 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties @@ -15,7 +15,7 @@ approvedLanguage=Language approvedScriptLength=Length approvedScriptLength_tooltip=Length of the script in characters usageCount=# of uses -usageCount_tooltip=Usage since the introduction of metadata +usageCount_tooltip=Uses since the introduction of metadata lastTimeUsed=Date of last use wasPreapproved_header_tooltip=The presence of a green tick means the script was approved during the configuration. \ Otherwise it was created by a user without the permission to run scripts and was then approved using this page. @@ -25,17 +25,17 @@ lastKnownApproverLogin=Last approver ## table content noRequesterSinceMetadata=N/A -noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script does not provide the user information. +noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script did not provide user information. noRequestApprovalTimeSinceMetadata=N/A noRequestApprovalTimeSinceMetadata_tooltip=It was requested before the introduction of metadata. noContextItemSinceMetadata=N/A -noContextItemSinceMetadata_tooltip=The context item was not provided by the code asking for the approval. +noContextItemSinceMetadata_tooltip=The context item was not provided by the code asking for approval. emptyMetadata=There is no metadata for this approval emptyMetadata_tooltip=If the script is used after the metadata introduction, some metadata will be displayed here. \ It means that an unused approval will not have metadata and could be reasonably revoked after a certain period of time. noScriptCodeSinceMetadata=N/A -noScriptCodeSinceMetadata_tooltip=The approval was given while the metadata were not gathered. +noScriptCodeSinceMetadata_tooltip=The approval was given while the metadata was not gathered. approvedContextUser=Requester approvedContextItem=Context wasPreapproved_tooltip=The script content was approved directly during configuration, meaning it comes from a user with the permission to run scripts. @@ -59,5 +59,5 @@ approvedExpand_tooltip=Expand the source code visualization approvedMinimize=minimize code approvedMinimize_tooltip=Minimize the source code visualization scriptContentLoadingMessage=Loading... -scriptContentLoadError=Impossible to find the script content. It was perhaps removed manually. On next usage this should be self-repaired. -scriptContentLoadError403=Impossible to request the script content due to lack of authorization. Try reloading the page. +scriptContentLoadError=Unable to find the script content. It was perhaps removed manually. On next use this should be self-repaired. +scriptContentLoadError403=Unable to request the script content due to lack of authorization. Try reloading the page. diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly index 9eb037e11..4b543223c 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_pending.jelly @@ -67,4 +67,4 @@ }); } - \ No newline at end of file + From 9bcceb4e4d128c4d3fc12b528c357d9e91e68243 Mon Sep 17 00:00:00 2001 From: Wadeck Follonier Date: Tue, 18 Aug 2020 15:39:41 +0200 Subject: [PATCH 14/21] Various updates after reviews - Update the code related to the XSS - Remove the hash column - Remove the length column - Adjust the CSS to match the style after the table revamp (not before as it's not compatible) --- .../scripts/Messages.properties | 4 +- .../scripts/ScriptApproval/_resources.css | 8 - .../scripts/ScriptApproval/index.properties | 3 - .../tabs/classPath_approved.jelly | 4 +- .../tabs/classPath_pending.jelly | 4 +- .../tabs/fullScript_approved.jelly | 376 +++++++++--------- .../tabs/fullScript_approved.properties | 14 +- .../tabs/fullScript_pending.jelly | 207 +++++----- .../tabs/fullScript_pending.properties | 9 +- .../tabs/signature_approved.jelly | 2 +- .../tabs/signature_pending.jelly | 2 +- 11 files changed, 297 insertions(+), 336 deletions(-) delete mode 100644 src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties index 698a24e72..043dcda81 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/Messages.properties @@ -7,5 +7,5 @@ ScriptApproval.tab.fullScriptPending=Scripts - pending approval ScriptApproval.tab.fullScriptApproved=Scripts - approved ScriptApproval.tab.signaturePending=Signatures - pending approval ScriptApproval.tab.signatureApproved=Signatures - approved -ScriptApproval.tab.classPathPending=Class paths - pending approval -ScriptApproval.tab.classPathApproved=Class paths - approved +ScriptApproval.tab.classPathPending=Classpaths - pending approval +ScriptApproval.tab.classPathApproved=Classpaths - approved diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css index 4f81f9cf4..58607d187 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/_resources.css @@ -23,14 +23,6 @@ background-color: #6c757d; } -.script-approval-page .custom-pane-frame-padding { - padding: 8px 12px; - - /* border part stolen from pane-frame */ - border: solid 1px #f0f0f0; - border-radius: 4px; -} - .script-approval-page h2 ~ h2 { margin-top: 35px; } diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties deleted file mode 100644 index d157bcb43..000000000 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/index.properties +++ /dev/null @@ -1,3 +0,0 @@ -fullScriptApproval=Full script approvals -signatureApproval=Signature approvals -classPathApproval=Classpath entry approvals diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly index 5f515ad9f..969764589 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_approved.jelly @@ -42,7 +42,9 @@ } }); block.insert(deleteButton); - block.insert("<code title='" + e.hash + "'>" + e.path + "</code>"); + var code = new Element('code', { 'title': e.hash }); + code.textContent = e.path; + block.insert(code); $('approvedClasspathEntries').insert(block); }); $('approvedClasspathEntries').show(); diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly index 55e09dc9c..5c391284f 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/classPath_pending.jelly @@ -41,7 +41,9 @@ }); block.insert(approveButton); block.insert(denyButton); - block.insert("<code title='" + e.hash + "'>" + e.path + "</code>"); + var code = new Element('code', { 'title': e.hash }); + code.textContent = e.path; + block.insert(code); $('pendingClasspathEntries').insert(block); }); diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly index 70e499864..bdd30f3a5 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.jelly @@ -1,204 +1,192 @@ -
+
-

- ${%approvedScripts} -

- -
- -
- ${%metadataGatheringDisabled} -
-
- - -

- ${%noApprovedScripts} -

-
- - - - - - - - - - - - - - - + +
+ ${%metadataGatheringDisabled} +
+
+ + +

+ ${%noApprovedScripts} +

+
+ +
${%approvedHash}${%approvedExpandCode}${%approvedLanguage}${%approvedScriptLength}${%approvedContextUser}${%approvedContextItem}${%usageCount}${%lastTimeUsed} - - ${%lastApprovalTime}${%lastKnownApproverLogin}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - -
${%approvedExpandCode}${%approvedLanguage}${%approvedContextUser}${%approvedContextItem}${%usageCount}${%lastTimeUsed} + + ${%lastApprovalTime}${%lastKnownApproverLogin}
+ + + ${%emptyMetadata} + + + +
+ ${%approvedExpand} + ${%approvedMinimize} +
+
+ + + ${%noScriptCodeSinceMetadata} + + +
+
${language.displayName} + + + + ${user} + + + + ${%noRequesterSinceMetadata} + + + + + + + + ${contextItem.fullDisplayName} + + + + ${%noContextItemSinceMetadata} + + + + + + + + ${usageCount} + + + + ${%noUsageCountSinceMetadata} + + + + + + + + + + + + ${%notUsedSinceMetadata} + + + + + + + + + + + + + + + + + + ${%notApprovedSinceMetadata} + + + + + + + + ${lastKnownApproverLogin} + + + + ${%noKnownApproverSinceMetadata} + + + +
- - - + +
+
+ ${%scriptContentLoadingMessage} +
+
+ ${%scriptContentLoadError} +
+
+ ${%scriptContentLoadError403} +
- ${%emptyMetadata} - - - -
- ${%approvedExpand} - ${%approvedMinimize} -
-
- - - ${%noScriptCodeSinceMetadata} - - -
-
${language.displayName}${metadata.scriptLength} - - - - ${user} - - - - ${%noRequesterSinceMetadata} - - - - - - - - ${contextItem.fullDisplayName} - - - - ${%noContextItemSinceMetadata} - - - - - - - - ${usageCount} - - - - ${%noUsageCountSinceMetadata} - - - - - - - - - - - - ${%notUsedSinceMetadata} - - - - - - - - - - - - - - - - - - ${%notApprovedSinceMetadata} - - - - - - - - ${lastKnownApproverLogin} - - - - ${%noKnownApproverSinceMetadata} - - - -
-
- ${%scriptContentLoadingMessage} -
-
- ${%scriptContentLoadError} -
-
- ${%scriptContentLoadError403} -
-
+ + + -

- ${%approvedNoSelectedItemError} -

+

+ ${%approvedNoSelectedItemError} +

-
- - - -
-
-
-
+
+ + + +
+ +
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties index 62cfc7627..4d0f6bd6b 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_approved.properties @@ -1,19 +1,13 @@ -approvedScripts=Approved scripts - # approved scripts -metadataGatheringDisabled=The metadata gathering was disabled. \ +metadataGatheringDisabled=Metadata gathering disabled. \

You can re-enable it permanently by removing the system property:
\ org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.metadataGathering

\

or temporarily (until restart) by running this in the Script Console:
\ org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval.METADATA_GATHERING = true

## table headers -approvedHash=Hash -approvedHash_tooltip=The hash could be seen as a signature of the script. It's used as an identifier to reference the content of the script. approvedExpandCode=Code approvedLanguage=Language -approvedScriptLength=Length -approvedScriptLength_tooltip=Length of the script in characters usageCount=# of uses usageCount_tooltip=Uses since the introduction of metadata lastTimeUsed=Date of last use @@ -25,7 +19,8 @@ lastKnownApproverLogin=Last approver ## table content noRequesterSinceMetadata=N/A -noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script did not provide user information. +noRequesterSinceMetadata_tooltip=The requester was not recorded. \ + It could be due to the request being created before the metadata introduction or the code calling this script did not provide user information. noRequestApprovalTimeSinceMetadata=N/A noRequestApprovalTimeSinceMetadata_tooltip=It was requested before the introduction of metadata. noContextItemSinceMetadata=N/A @@ -50,7 +45,8 @@ noKnownApproverSinceMetadata=N/A noKnownApproverSinceMetadata_tooltip=It was not approved since the introduction of metadata. revokeApproval=Revoke all selected -revokeApproval_tooltip=Revoking an approved script can have an impact on the jobs using it. The next time an attempt to execute the script is made, a new approval will be required. +revokeApproval_tooltip=Revoking an approved script can have an impact on the jobs using it. \ + The next time an attempt to execute the script is made, a new approval will be required. revokeApproval_confirm=Really delete all selected approvals? Any existing scripts will need to be requeued and reapproved. approvedNoSelectedItemError=Please select at least one line. diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly index 5796cabc3..d8282c27b 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.jelly @@ -1,117 +1,106 @@ -
+
-

- ${%pendingScripts} -

-
- - -

- ${%noPendingScripts} -

-
- - + + +

+ ${%noPendingScripts} +

+
+ +
+ + + + + + + + + + + + + + + + + - - - - - - - - + - - - - - - - - - - - - - - - -
${%pendingExpandCode}${%pendingLanguage}${%contextUser}${%contextItem}${%requestApprovalTime}
+ + +
+ ${%pendingExpand} + ${%pendingMinimize} +
+
${ps.language.displayName} + + + + ${user} + + + + ${%noRequesterSinceMetadata} + + + + + + + + ${contextItem.fullDisplayName} + + + + ${%noContextItemSinceMetadata} + + + + + + + + + + + + ${%noRequestApprovalTimeSinceMetadata} + + + +
${%pendingHash}${%pendingExpandCode}${%pendingLanguage}${%pendingScriptLength}${%contextUser}${%contextItem}${%requestApprovalTime} + +
- - - - -
- ${%pendingExpand} - ${%pendingMinimize} -
-
${ps.language.displayName}${ps.script.size()} - - - - ${user} - - - - ${%noRequesterSinceMetadata} - - - - - - - - ${contextItem.fullDisplayName} - - - - ${%noContextItemSinceMetadata} - - - - - - - - - - - - ${%noRequestApprovalTimeSinceMetadata} - - - -
- -
- -

- ${%pendingNoSelectedItemError} -

- -
- - - - - - -
-
-
-
+ + + +

+ ${%pendingNoSelectedItemError} +

+ +
+ + + + + + +
+ +
diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties index ab56d598a..1ed3027cb 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/fullScript_pending.properties @@ -1,13 +1,7 @@ -pendingScripts=Scripts pending approval - # pending scripts ## table headers -pendingHash=Hash -pendingHash_tooltip=The hash could be seen as a signature of the script. It's used as an identifier to reference the content of the script. pendingExpandCode=Code pendingLanguage=Language -pendingScriptLength=Length -pendingScriptLength_tooltip=Length of the script in characters contextUser=Requester contextItem=Context requestApprovalTime=Request date @@ -15,7 +9,8 @@ requestApprovalTime_tooltip=Inform about when the approval request was made. ## table content noRequesterSinceMetadata=N/A -noRequesterSinceMetadata_tooltip=The requester was not recorded. It could be due to the request being created before the metadata introduction or the code calling this script did not provide user information. +noRequesterSinceMetadata_tooltip=The requester was not recorded. \ + It could be due to the request being created before the metadata introduction or the code calling this script did not provide user information. noRequestApprovalTimeSinceMetadata=N/A noRequestApprovalTimeSinceMetadata_tooltip=It was requested before the introduction of metadata. noContextItemSinceMetadata=N/A diff --git a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly index ea52a11d2..2f824653c 100644 --- a/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly +++ b/src/main/resources/org/jenkinsci/plugins/scriptsecurity/scripts/ScriptApproval/tabs/signature_approved.jelly @@ -1,6 +1,6 @@ -
+
Signatures already approved: