diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 1ce8caa55983fe13dbabf6bbf55d162fef4cd787..6ea71e4628bd637ffd93e92d6b8cbbd2a32cc47e 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -25,6 +25,8 @@ */ package hudson; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; import hudson.cli.CLICommand; import hudson.console.ConsoleAnnotationDescriptor; import hudson.console.ConsoleAnnotatorFactory; @@ -33,6 +35,7 @@ import hudson.model.ParameterDefinition.ParameterDescriptor; import hudson.search.SearchableModelObject; import hudson.security.AccessControlled; import hudson.security.AuthorizationStrategy; +import hudson.security.GlobalSecurityConfiguration; import hudson.security.Permission; import hudson.security.SecurityRealm; import hudson.security.captcha.CaptchaSupport; @@ -57,6 +60,8 @@ import hudson.views.MyViewsTabBar; import hudson.views.ViewsTabBar; import hudson.widgets.RenderOnDemandClosure; import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; +import jenkins.model.GlobalConfigurationCategory.Unclassified; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithContextMenu; import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken; @@ -74,7 +79,6 @@ import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.jelly.InternationalizedStringExpression.RawHtmlArgument; -import javax.management.modelmbean.DescriptorSupport; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; @@ -769,32 +773,11 @@ public class Functions { * Perhaps it is better to introduce another annotation element? But then, * extensions shouldn't normally concern themselves about ordering too much, and the only reason * we needed this for {@link GlobalConfiguration}s are for backward compatibility. + * + * @param predicate + * Filter the descriptors based on {@link GlobalConfigurationCategory} */ - public static Collection getSortedDescriptorsForGlobalConfig() { - class Tag implements Comparable { - double ordinal; - String hierarchy; - Descriptor d; - - Tag(double ordinal, Descriptor d) { - this.ordinal = ordinal; - this.d = d; - this.hierarchy = buildSuperclassHierarchy(d.clazz, new StringBuilder()).toString(); - } - - private StringBuilder buildSuperclassHierarchy(Class c, StringBuilder buf) { - Class sc = c.getSuperclass(); - if (sc!=null) buildSuperclassHierarchy(sc,buf).append(':'); - return buf.append(c.getName()); - } - - public int compareTo(Tag that) { - int r = Double.compare(this.ordinal, that.ordinal); - if (r!=0) return -r; // descending for ordinal - return this.hierarchy.compareTo(that.hierarchy); - } - } - + public static Collection getSortedDescriptorsForGlobalConfig(Predicate predicate) { ExtensionList exts = Jenkins.getInstance().getExtensionList(Descriptor.class); List r = new ArrayList(exts.size()); @@ -802,7 +785,13 @@ public class Functions { Descriptor d = c.getInstance(); if (d.getGlobalConfigPage()==null) continue; - r.add(new Tag(d instanceof GlobalConfiguration ? c.ordinal() : 0, d)); + if (d instanceof GlobalConfiguration) { + if (predicate.apply(((GlobalConfiguration)d).getCategory())) + r.add(new Tag(c.ordinal(), d)); + } else { + if (predicate.apply(GlobalConfigurationCategory.get(Unclassified.class))) + r.add(new Tag(0, d)); + } } Collections.sort(r); @@ -812,7 +801,37 @@ public class Functions { return DescriptorVisibilityFilter.apply(Jenkins.getInstance(),answer); } + public static Collection getSortedDescriptorsForGlobalConfig() { + return getSortedDescriptorsForGlobalConfig(Predicates.alwaysTrue()); + } + + public static Collection getSortedDescriptorsForGlobalConfigNoSecurity() { + return getSortedDescriptorsForGlobalConfig(Predicates.not(GlobalSecurityConfiguration.FILTER)); + } + + private static class Tag implements Comparable { + double ordinal; + String hierarchy; + Descriptor d; + + Tag(double ordinal, Descriptor d) { + this.ordinal = ordinal; + this.d = d; + this.hierarchy = buildSuperclassHierarchy(d.clazz, new StringBuilder()).toString(); + } + + private StringBuilder buildSuperclassHierarchy(Class c, StringBuilder buf) { + Class sc = c.getSuperclass(); + if (sc!=null) buildSuperclassHierarchy(sc,buf).append(':'); + return buf.append(c.getName()); + } + public int compareTo(Tag that) { + int r = Double.compare(this.ordinal, that.ordinal); + if (r!=0) return -r; // descending for ordinal + return this.hierarchy.compareTo(that.hierarchy); + } + } /** * Computes the path to the icon of the given action * from the context path. diff --git a/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java index 9997405bd14d8b64ba084480d0f32970035e57c3..31dd6855e38c2f3d6341446c147f0b85a31be1ae 100644 --- a/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java +++ b/core/src/main/java/hudson/security/GlobalSecurityConfiguration.java @@ -23,47 +23,74 @@ */ package hudson.security; +import com.google.common.base.Predicate; +import hudson.BulkChange; import hudson.Extension; +import hudson.Functions; import hudson.markup.MarkupFormatter; -import jenkins.model.GlobalConfiguration; +import hudson.model.Descriptor; +import hudson.model.Descriptor.FormException; +import hudson.model.ManagementLink; +import hudson.util.FormApply; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.servlet.ServletException; + +import jenkins.model.GlobalConfigurationCategory; import jenkins.model.Jenkins; import jenkins.util.ServerTcpPort; import net.sf.json.JSONObject; -import org.kohsuke.stapler.StaplerRequest; -import java.io.IOException; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; /** * Security configuration. * * @author Kohsuke Kawaguchi */ -@Extension(ordinal=200) -public class GlobalSecurityConfiguration extends GlobalConfiguration { +@Extension(ordinal = Integer.MAX_VALUE - 210) +public class GlobalSecurityConfiguration extends ManagementLink { + + private static final Logger LOGGER = Logger.getLogger(GlobalSecurityConfiguration.class.getName()); + public MarkupFormatter getMarkupFormatter() { return Jenkins.getInstance().getMarkupFormatter(); } - + public int getSlaveAgentPort() { return Jenkins.getInstance().getSlaveAgentPort(); } - - @Override - public boolean configure(StaplerRequest req, JSONObject json) throws FormException { + + public synchronized void doConfigure(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { // for compatibility reasons, the actual value is stored in Jenkins - Jenkins j = Jenkins.getInstance(); + BulkChange bc = new BulkChange(Jenkins.getInstance()); + try{ + boolean result = configure(req, req.getSubmittedForm()); + LOGGER.log(Level.FINE, "security saved: "+result); + Jenkins.getInstance().save(); + FormApply.success(req.getContextPath()+"/manage").generateResponse(req, rsp, null); + } finally { + bc.commit(); + } + } + public boolean configure(StaplerRequest req, JSONObject json) throws hudson.model.Descriptor.FormException { + // for compatibility reasons, the actual value is stored in Jenkins + Jenkins j = Jenkins.getInstance(); + j.checkPermission(Jenkins.ADMINISTER); if (json.has("useSecurity")) { JSONObject security = json.getJSONObject("useSecurity"); j.setSecurityRealm(SecurityRealm.all().newInstanceFromRadioList(security, "realm")); j.setAuthorizationStrategy(AuthorizationStrategy.all().newInstanceFromRadioList(security, "authorization")); - try { j.setSlaveAgentPort(new ServerTcpPort(security.getJSONObject("slaveAgentPort")).getPort()); } catch (IOException e) { - throw new FormException(e,"slaveAgentPortType"); + throw new hudson.model.Descriptor.FormException(e, "slaveAgentPortType"); } - if (security.has("markupFormatter")) { j.setMarkupFormatter(req.bindJSON(MarkupFormatter.class, security.getJSONObject("markupFormatter"))); } else { @@ -73,7 +100,51 @@ public class GlobalSecurityConfiguration extends GlobalConfiguration { j.disableSecurity(); } - return true; + // persist all the additional security configs + boolean result = true; + for(Descriptor d : Functions.getSortedDescriptorsForGlobalConfig(FILTER)){ + result &= configureDescriptor(req,json,d); + } + + return result; + } + + private boolean configureDescriptor(StaplerRequest req, JSONObject json, Descriptor d) throws FormException { + // collapse the structure to remain backward compatible with the JSON structure before 1. + String name = d.getJsonSafeClassName(); + JSONObject js = json.has(name) ? json.getJSONObject(name) : new JSONObject(); // if it doesn't have the property, the method returns invalid null object. + json.putAll(js); + return d.configure(req, js); + } + + @Override + public String getDisplayName() { + return Messages.GlobalSecurityConfiguration_DisplayName(); + } + + @Override + public String getDescription() { + return Messages.GlobalSecurityConfiguration_Description(); + } + + @Override + public String getIconFileName() { + return "secure.png"; + } + + @Override + public String getUrlName() { + return "configureSecurity"; + } + + @Override + public Permission getRequiredPermission() { + return Jenkins.ADMINISTER; } -} + public static Predicate FILTER = new Predicate() { + public boolean apply(GlobalConfigurationCategory input) { + return input instanceof GlobalConfigurationCategory.Security; + } + }; +} diff --git a/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java b/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java index 29a22202b1d8e2bc8ced3c8fdb4f04ed4e31950f..904dbb37d53c315c0917432841bc3f8eca93562a 100644 --- a/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java +++ b/core/src/main/java/hudson/security/csrf/GlobalCrumbIssuerConfiguration.java @@ -25,6 +25,7 @@ package hudson.security.csrf; import hudson.Extension; import jenkins.model.GlobalConfiguration; +import jenkins.model.GlobalConfigurationCategory; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.kohsuke.stapler.StaplerRequest; @@ -36,6 +37,11 @@ import org.kohsuke.stapler.StaplerRequest; */ @Extension(ordinal=195) // immediately after the security setting public class GlobalCrumbIssuerConfiguration extends GlobalConfiguration { + @Override + public GlobalConfigurationCategory getCategory() { + return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); + } + @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { // for compatibility reasons, the actual value is stored in Jenkins diff --git a/core/src/main/java/jenkins/model/GlobalConfiguration.java b/core/src/main/java/jenkins/model/GlobalConfiguration.java index 5fd6a0818dd00abd44c95672c16a34dacbfb937f..b6e873f952c8eff32be59dae69c2de903b9d0242 100644 --- a/core/src/main/java/jenkins/model/GlobalConfiguration.java +++ b/core/src/main/java/jenkins/model/GlobalConfiguration.java @@ -31,6 +31,15 @@ public abstract class GlobalConfiguration extends Descriptor + * To facilitate the separation of the global configuration into multiple pages, tabs, and so on, + * {@link GlobalConfiguration}s are classified into categories (such as "security", "tools", as well + * as the catch all "unclassified".) Categories themselves are extensible — plugins may introduce + * its own category as well, although that should only happen if you are creating a big enough subsystem. + * + *

+ * The primary purpose of this is to enable future UIs to split the global configurations to + * smaller pieces that can be individually looked at and updated. + * + * @author Kohsuke Kawaguchi + * @since 1.494 + * @see GlobalConfiguration + */ +public abstract class GlobalConfigurationCategory implements ExtensionPoint, ModelObject { + /** + * One-line plain text message that explains what this category is about. + * This can be used in the UI to help the user pick the right category. + * + * The text should be longer than {@link #getDisplayName()} + */ + public abstract String getShortDescription(); + + /** + * Returns all the registered {@link GlobalConfiguration} descriptors. + */ + public static ExtensionList all() { + return Jenkins.getInstance().getExtensionList(GlobalConfigurationCategory.class); + } + + public static T get(Class type) { + return all().get(type); + } + + /** + * This category represents the catch-all I-dont-know-what-category-it-is instance, + * used for those {@link GlobalConfiguration}s that don't really deserve/need a separate + * category. + * + * Also used for backward compatibility. All {@link GlobalConfiguration}s without + * explicit category gets this as the category. + * + * In the current UI, this corresponds to the /configure link. + */ + @Extension + public static class Unclassified extends GlobalConfigurationCategory { + @Override + public String getShortDescription() { + return jenkins.management.Messages.ConfigureLink_Description(); + } + + public String getDisplayName() { + return jenkins.management.Messages.ConfigureLink_DisplayName(); + } + } + + /** + * Security related configurations. + */ + @Extension + public static class Security extends GlobalConfigurationCategory { + @Override + public String getShortDescription() { + return Messages.GlobalSecurityConfiguration_Description(); + } + + public String getDisplayName() { + return hudson.security.Messages.GlobalSecurityConfiguration_DisplayName(); + } + } +} diff --git a/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/config.groovy b/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/config.groovy deleted file mode 100644 index a9d988b0d270a7830b1b52b6a2dc1f100810fee2..0000000000000000000000000000000000000000 --- a/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/config.groovy +++ /dev/null @@ -1,21 +0,0 @@ -package hudson.security.GlobalSecurityConfiguration - -import hudson.security.SecurityRealm -import hudson.security.AuthorizationStrategy - -def f=namespace(lib.FormTagLib) - -f.optionalBlock( field:"useSecurity", title:_("Enable security"), checked:app.useSecurity) { - f.entry (title:_("TCP port for JNLP slave agents"), field:"slaveAgentPort") { - f.serverTcpPort() - } - - f.dropdownDescriptorSelector(title:_("Markup Formatter"),field:"markupFormatter") - - f.entry(title:_("Access Control")) { - table(style:"width:100%") { - f.descriptorRadioList(title:_("Security Realm"),varName:"realm", instance:app.securityRealm, descriptors:SecurityRealm.all()) - f.descriptorRadioList(title:_("Authorization"), varName:"authorization", instance:app.authorizationStrategy, descriptors:AuthorizationStrategy.all()) - } - } -} diff --git a/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/index.groovy b/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/index.groovy new file mode 100644 index 0000000000000000000000000000000000000000..c9509baf0dc6f1af10a96cb90f842bd7a1372d04 --- /dev/null +++ b/core/src/main/resources/hudson/security/GlobalSecurityConfiguration/index.groovy @@ -0,0 +1,60 @@ +package hudson.security.GlobalSecurityConfiguration + +import hudson.security.SecurityRealm +import hudson.markup.MarkupFormatterDescriptor +import hudson.security.AuthorizationStrategy +import jenkins.model.GlobalConfiguration +import hudson.Functions +import hudson.model.Descriptor + +def f=namespace(lib.FormTagLib) +def l=namespace(lib.LayoutTagLib) +def st=namespace("jelly:stapler") + +l.layout(norefresh:true, permission:app.ADMINISTER, title:my.displayName) { + l.main_panel { + h1 { + img(src:"${imagesURL}/48x48/secure.png", height:48,width:48) + text(my.displayName) + } + + p() + div(class:"behavior-loading", _("LOADING")) + f.form(method:"post",name:"config",action:"configure") { + set("instance",my); + + f.optionalBlock( field:"useSecurity", title:_("Enable security"), checked:app.useSecurity) { + f.entry (title:_("TCP port for JNLP slave agents"), field:"slaveAgentPort") { + f.serverTcpPort() + } + + f.dropdownDescriptorSelector(title:_("Markup Formatter"),descriptors: MarkupFormatterDescriptor.all(), field: 'markupFormatter') + + f.entry(title:_("Access Control")) { + table(style:"width:100%") { + f.descriptorRadioList(title:_("Security Realm"),varName:"realm", instance:app.securityRealm, descriptors:SecurityRealm.all()) + f.descriptorRadioList(title:_("Authorization"), varName:"authorization", instance:app.authorizationStrategy, descriptors:AuthorizationStrategy.all()) + } + } + } + + Functions.getSortedDescriptorsForGlobalConfig(my.FILTER).each { Descriptor descriptor -> + set("descriptor",descriptor) + set("instance",descriptor) + f.rowSet(name:descriptor.jsonSafeClassName) { + st.include(from:descriptor, page:descriptor.globalConfigPage) + } + } + + f.block { + div(id:"bottom-sticker") { + div(class:"bottom-sticker-inner") { + f.submit(value:_("Save")) + f.apply() + } + } + } + } + } +} + diff --git a/core/src/main/resources/hudson/security/Messages.properties b/core/src/main/resources/hudson/security/Messages.properties index 9b4f54a01bc059fc7765fd8fe9748021d4cf2713..3ddab58365afe967614b25b02ff7aa6286fb31f3 100644 --- a/core/src/main/resources/hudson/security/Messages.properties +++ b/core/src/main/resources/hudson/security/Messages.properties @@ -19,6 +19,8 @@ # 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. +GlobalSecurityConfiguration.DisplayName=Configure Global Security +GlobalSecurityConfiguration.Description=Secure Jenkins; define who is allowed to access/use the system. GlobalMatrixAuthorizationStrategy.DisplayName=Matrix-based security diff --git a/core/src/main/resources/jenkins/model/Jenkins/configure.jelly b/core/src/main/resources/jenkins/model/Jenkins/configure.jelly index 69c778d9d42148ce6703adfc4d2fd57a14fa59f2..bacb2e00c5d8a86c6f34ddb2866fd03dcb18f978 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/configure.jelly +++ b/core/src/main/resources/jenkins/model/Jenkins/configure.jelly @@ -53,7 +53,7 @@ THE SOFTWARE. - + diff --git a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java index c7cc4812504fceb83f3f302f829c94edabd41a28..1acb0b7f605a474797ec0f0102865be902d65cf7 100644 --- a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java +++ b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java @@ -24,6 +24,7 @@ */ package org.jvnet.hudson.test; +import com.gargoylesoftware.htmlunit.AlertHandler; import com.gargoylesoftware.htmlunit.html.HtmlImage; import com.google.inject.Injector; import hudson.ClassicPluginStrategy; @@ -1659,6 +1660,12 @@ public abstract class HudsonTestCase extends TestCase implements RootAction { } }); + setAlertHandler(new AlertHandler() { + public void handleAlert(Page page, String message) { + throw new AssertionError("Alert dialog poped up: "+message); + } + }); + // avoid a hang by setting a time out. It should be long enough to prevent // false-positive timeout on slow systems setTimeout(60*1000); diff --git a/war/src/main/webapp/images/16x16/secure.png b/war/src/main/webapp/images/16x16/secure.png new file mode 100644 index 0000000000000000000000000000000000000000..3f97bbbbd8ee0733f987ac7b65a43753dc248136 Binary files /dev/null and b/war/src/main/webapp/images/16x16/secure.png differ diff --git a/war/src/main/webapp/images/24x24/lock.png b/war/src/main/webapp/images/24x24/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..72a8b14a9dd8b22357af7bb5eb0103b84a4eeaee Binary files /dev/null and b/war/src/main/webapp/images/24x24/lock.png differ diff --git a/war/src/main/webapp/images/24x24/secure.png b/war/src/main/webapp/images/24x24/secure.png new file mode 100644 index 0000000000000000000000000000000000000000..1a03cc3c2a1f67cb3d8508cf0d1d2e38a8de4bc8 Binary files /dev/null and b/war/src/main/webapp/images/24x24/secure.png differ diff --git a/war/src/main/webapp/images/32x32/lock.png b/war/src/main/webapp/images/32x32/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..c20b0021f2d37d19efd913e5ca4d3e42cd71b26a Binary files /dev/null and b/war/src/main/webapp/images/32x32/lock.png differ diff --git a/war/src/main/webapp/images/32x32/secure.png b/war/src/main/webapp/images/32x32/secure.png new file mode 100644 index 0000000000000000000000000000000000000000..5e06c8db797eaa937f49a51f2d9c61855f8a121e Binary files /dev/null and b/war/src/main/webapp/images/32x32/secure.png differ diff --git a/war/src/main/webapp/images/48x48/lock.png b/war/src/main/webapp/images/48x48/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..9e11e32eb0193f914f2a72455e79d8ea616525e8 Binary files /dev/null and b/war/src/main/webapp/images/48x48/lock.png differ diff --git a/war/src/main/webapp/images/48x48/secure.png b/war/src/main/webapp/images/48x48/secure.png index 95f214d93268740c23e76f8c630944d7ae717f65..2b201553f02242ccdb38a3346780cb54cebfa4db 100644 Binary files a/war/src/main/webapp/images/48x48/secure.png and b/war/src/main/webapp/images/48x48/secure.png differ