diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 798a51f9ab..1c104ae410 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -1189,7 +1189,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas break; } updateCenter.persistInstallStatus(); - jenkins.setInstallState(InstallState.INITIAL_PLUGINS_INSTALLED); + jenkins.setInstallState(InstallState.INITIAL_PLUGINS_INSTALLING.getNextState()); InstallUtil.saveLastExecVersion(); } }.start(); diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java index a8e9122e7c..e595f0a93d 100644 --- a/core/src/main/java/hudson/model/UpdateCenter.java +++ b/core/src/main/java/hudson/model/UpdateCenter.java @@ -57,7 +57,8 @@ import jenkins.install.InstallState; import jenkins.install.InstallUtil; import jenkins.model.Jenkins; import jenkins.util.io.OnMaster; -import net.sf.json.JSONArray; +import net.sf.json.JSONObject; + import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContext; import org.apache.commons.codec.binary.Base64; @@ -73,7 +74,6 @@ import javax.annotation.Nonnull; import javax.net.ssl.SSLHandshakeException; import javax.servlet.ServletException; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -81,12 +81,12 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; -import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; @@ -131,7 +131,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST; */ @ExportedBean public class UpdateCenter extends AbstractModelObject implements Saveable, OnMaster { - + private static final String UPDATE_CENTER_URL = System.getProperty(UpdateCenter.class.getName()+".updateCenterUrl","http://updates.jenkins-ci.org/"); /** @@ -142,7 +142,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas @Restricted(NoExternalUse.class) public static final String ID_UPLOAD = "_upload"; - + /** * {@link ExecutorService} that performs installation. * @since 1.501 @@ -155,7 +155,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas */ protected final ExecutorService updateService = Executors.newCachedThreadPool( new NamingThreadFactory(new DaemonThreadFactory(), "Update site data downloader")); - + /** * List of created {@link UpdateCenterJob}s. Access needs to be synchronized. */ @@ -311,29 +311,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } } - /** - * Called to bypass install wizard - */ - @Restricted(DoNotUse.class) // WebOnly - public HttpResponse doCompleteInstall() { - if(isRestartRequiredForCompletion()) { - Jenkins.getActiveInstance().setInstallState(InstallState.RESTART); - } - InstallUtil.saveLastExecVersion(); - Jenkins.getActiveInstance().setInstallState(InstallState.INITIAL_PLUGINS_INSTALLED); - return HttpResponses.okJSON(); - } - /** * Called to determine if there was an incomplete installation, what the statuses of the plugins are */ @Restricted(DoNotUse.class) // WebOnly public HttpResponse doIncompleteInstallStatus() { try { - Map jobs = InstallUtil.getPersistedInstallStatus(); - if(jobs == null) { - jobs = Collections.emptyMap(); - } + Map jobs = InstallUtil.getPersistedInstallStatus(); + if(jobs == null) { + jobs = Collections.emptyMap(); + } return HttpResponses.okJSON(jobs); } catch (Exception e) { return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage())); @@ -353,16 +340,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas if (job instanceof InstallationJob) { InstallationJob installationJob = (InstallationJob) job; if(!installationJob.status.isSuccess()) { - activeInstalls = true; + activeInstalls = true; } } } if(activeInstalls) { - InstallUtil.persistInstallStatus(jobs); // save this info + InstallUtil.persistInstallStatus(jobs); // save this info } else { - InstallUtil.clearInstallStatus(); // clear this info + InstallUtil.clearInstallStatus(); // clear this info } } @@ -379,7 +366,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public HttpResponse doInstallStatus(StaplerRequest request) { try { String correlationId = request.getParameter("correlationId"); + Map response = new HashMap<>(); + response.put("state", Jenkins.getInstance().getInstallState().name()); List> installStates = new ArrayList<>(); + response.put("jobs", installStates); List jobCopy = getJobs(); for (UpdateCenterJob job : jobCopy) { @@ -400,7 +390,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } } } - return HttpResponses.okJSON(JSONArray.fromObject(installStates)); + return HttpResponses.okJSON(JSONObject.fromObject(response)); } catch (Exception e) { return HttpResponses.errorJSON(String.format("ERROR: %s", e.getMessage())); } @@ -558,7 +548,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } response.sendRedirect2("."); } - + /** * Cancel all scheduled jenkins restarts */ @@ -845,16 +835,16 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas return new ArrayList(pluginMap.values()); } - + /** * Ensure that all UpdateSites are up to date, without requiring a user to * browse to the instance. - * + * * @return a list of {@link FormValidation} for each updated Update Site - * @throws ExecutionException - * @throws InterruptedException + * @throws ExecutionException + * @throws InterruptedException * @since 1.501 - * + * */ public List updateAllSites() throws InterruptedException, ExecutionException { List > futures = new ArrayList>(); @@ -864,8 +854,8 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas futures.add(future); } } - - List results = new ArrayList(); + + List results = new ArrayList(); for (Future f : futures) { results.add(f.get()); } @@ -1211,7 +1201,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public String getErrorMessage() { return error != null ? error.getMessage() : null; } - + public Throwable getError() { return error; } @@ -1226,10 +1216,10 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas */ @Exported(inline=true) public volatile RestartJenkinsJobStatus status = new Pending(); - + /** * Cancel job - */ + */ public synchronized boolean cancel() { if (status instanceof Pending) { status = new Canceled(); @@ -1237,7 +1227,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas } return false; } - + public RestartJenkinsJob(UpdateSite site) { super(site); } @@ -1260,26 +1250,26 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public abstract class RestartJenkinsJobStatus { @Exported public final int id = iota.incrementAndGet(); - + } - + public class Pending extends RestartJenkinsJobStatus { @Exported public String getType() { return getClass().getSimpleName(); } } - + public class Running extends RestartJenkinsJobStatus { - + } - + public class Failure extends RestartJenkinsJobStatus { - + } - + public class Canceled extends RestartJenkinsJobStatus { - + } } @@ -1603,7 +1593,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas File baseDir = pm.rootDir; return new File(baseDir, plugin.name + ".jpi"); } - + private File getLegacyDestination() { File baseDir = pm.rootDir; return new File(baseDir, plugin.name + ".hpi"); @@ -1649,7 +1639,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas public String toString() { return super.toString()+"[plugin="+plugin.title+"]"; } - + /** * Called when the download is completed to overwrite * the old file with the new file. @@ -1704,7 +1694,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas File baseDir = pm.rootDir; final File legacy = new File(baseDir, plugin.name + ".hpi"); if(legacy.exists()){ - return legacy; + return legacy; } return new File(baseDir, plugin.name + ".jpi"); } diff --git a/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java b/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java index 8b2239f2f4..d34bb3059f 100644 --- a/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java +++ b/core/src/main/java/hudson/security/FullControlOnceLoggedInAuthorizationStrategy.java @@ -29,6 +29,7 @@ import jenkins.model.Jenkins; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import javax.inject.Inject; import java.util.Collections; @@ -36,30 +37,53 @@ import java.util.List; /** * {@link AuthorizationStrategy} that grants full-control to authenticated user - * (other than anonymous users.) + * and optionally read access to anonymous users * * @author Kohsuke Kawaguchi */ public class FullControlOnceLoggedInAuthorizationStrategy extends AuthorizationStrategy { + /** + * Whether to allow read access, default behavior + * previously was true + */ + private boolean allowAnonymousRead = true; + @DataBoundConstructor public FullControlOnceLoggedInAuthorizationStrategy() { } @Override public ACL getRootACL() { - return THE_ACL; + return allowAnonymousRead ? ANONYMOUS_READ : AUTHENTICATED_READ; } public List getGroups() { return Collections.emptyList(); } + + /** + * If true, anonymous read access will be allowed + */ + public boolean isAllowAnonymousRead() { + return allowAnonymousRead; + } + + @DataBoundSetter + public void setAllowAnonymousRead(boolean allowAnonymousRead) { + this.allowAnonymousRead = allowAnonymousRead; + } - private static final SparseACL THE_ACL = new SparseACL(null); + private static final SparseACL AUTHENTICATED_READ = new SparseACL(null); + private static final SparseACL ANONYMOUS_READ = new SparseACL(null); static { - THE_ACL.add(ACL.EVERYONE, Jenkins.ADMINISTER,true); - THE_ACL.add(ACL.ANONYMOUS, Jenkins.ADMINISTER,false); - THE_ACL.add(ACL.ANONYMOUS,Permission.READ,true); + ANONYMOUS_READ.add(ACL.EVERYONE, Jenkins.ADMINISTER,true); + ANONYMOUS_READ.add(ACL.ANONYMOUS, Jenkins.ADMINISTER,false); + ANONYMOUS_READ.add(ACL.ANONYMOUS, Permission.READ,true); + + AUTHENTICATED_READ.add(ACL.EVERYONE, Jenkins.ADMINISTER, true); + AUTHENTICATED_READ.add(ACL.ANONYMOUS, Jenkins.ADMINISTER, false); + AUTHENTICATED_READ.add(ACL.ANONYMOUS, Permission.READ, false); } /** diff --git a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java index ffaf8311d7..63404b636e 100644 --- a/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java +++ b/core/src/main/java/hudson/security/HudsonPrivateSecurityRealm.java @@ -1,18 +1,18 @@ /* * The MIT License - * + * * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, David Calavera, Seiji Sogabe - * + * * 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 @@ -29,6 +29,8 @@ import hudson.ExtensionList; import hudson.Util; import hudson.diagnosis.OldDataMonitor; import hudson.model.Descriptor; +import jenkins.install.InstallState; +import jenkins.install.SetupWizard; import jenkins.model.Jenkins; import hudson.model.ManagementLink; import hudson.model.ModelObject; @@ -97,7 +99,7 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea /** * If true, sign up is not allowed. *

- * This is a negative switch so that the default value 'false' remains compatible with older installations. + * This is a negative switch so that the default value 'false' remains compatible with older installations. */ private final boolean disableSignup; @@ -279,13 +281,24 @@ public class HudsonPrivateSecurityRealm extends AbstractPasswordBasedSecurityRea * This can be run by anyone, but only to create the very first user account. */ public void doCreateFirstAccount(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { - if(hasSomeUser()) { + boolean inSetup = !Jenkins.getInstance().getInstallState().isSetupComplete(); + if(!inSetup && hasSomeUser()) { rsp.sendError(SC_UNAUTHORIZED,"First user was already created"); return; } - User u = createAccount(req, rsp, false, "firstUser.jelly"); + String view = "firstUser.jelly"; + User admin = null; + if(inSetup) { + admin = getUser(SetupWizard.initialSetupAdminUserName); + view = "setupWizardFirstUser.jelly"; + } + User u = createAccount(req, rsp, false, view); if (u!=null) { tryToMakeAdmin(u); + if(admin != null) { + admin.delete(); + } + Jenkins.getInstance().setInstallState(InstallState.CREATE_ADMIN_USER.getNextState()); loginAndTakeBack(req, rsp, u); } } diff --git a/core/src/main/java/jenkins/install/InstallState.java b/core/src/main/java/jenkins/install/InstallState.java index 510feb6c78..c634180bc8 100644 --- a/core/src/main/java/jenkins/install/InstallState.java +++ b/core/src/main/java/jenkins/install/InstallState.java @@ -34,32 +34,63 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; @Restricted(NoExternalUse.class) public enum InstallState { /** - * New Jenkins install. + * The initial set up has been completed */ - NEW, + INITIAL_SETUP_COMPLETED(true, null), + /** + * Creating an admin user for an initial Jenkins install. + */ + CREATE_ADMIN_USER(false, INITIAL_SETUP_COMPLETED), /** * New Jenkins install. The user has kicked off the process of installing an * initial set of plugins (via the install wizard). */ - INITIAL_PLUGINS_INSTALLING, + INITIAL_PLUGINS_INSTALLING(false, CREATE_ADMIN_USER), /** - * New Jenkins install. The initial set of plugins are now installed. + * New Jenkins install. */ - INITIAL_PLUGINS_INSTALLED, + NEW(false, INITIAL_PLUGINS_INSTALLING), /** * Restart of an existing Jenkins install. */ - RESTART, + RESTART(true, INITIAL_SETUP_COMPLETED), /** * Upgrade of an existing Jenkins install. */ - UPGRADE, + UPGRADE(true, INITIAL_SETUP_COMPLETED), /** * Downgrade of an existing Jenkins install. */ - DOWNGRADE, + DOWNGRADE(true, INITIAL_SETUP_COMPLETED), /** * Jenkins started in test mode (JenkinsRule). */ - TEST + TEST(true, INITIAL_SETUP_COMPLETED), + /** + * Jenkins started in development mode: Bolean.getBoolean("hudson.Main.development"). + * Can be run normally with the -Djenkins.install.runSetupWizard=true + */ + DEVELOPMENT(true, INITIAL_SETUP_COMPLETED); + + private final boolean isSetupComplete; + private final InstallState nextState; + + private InstallState(boolean isSetupComplete, InstallState nextState) { + this.isSetupComplete = isSetupComplete; + this.nextState = nextState; + } + + /** + * Indicates the initial setup is complete + */ + public boolean isSetupComplete() { + return isSetupComplete; + } + + /** + * Gets the next state + */ + public InstallState getNextState() { + return nextState; + } } diff --git a/core/src/main/java/jenkins/install/InstallUtil.java b/core/src/main/java/jenkins/install/InstallUtil.java index e9622b1a9a..6fff100057 100644 --- a/core/src/main/java/jenkins/install/InstallUtil.java +++ b/core/src/main/java/jenkins/install/InstallUtil.java @@ -68,8 +68,15 @@ public class InstallUtil { * @return The type of "startup" currently under way in Jenkins. */ public static InstallState getInstallState() { - if (Functions.getIsUnitTest()) { - return InstallState.TEST; + // install wizard will always run if environment specified + if (!Boolean.getBoolean("jenkins.install.runSetupWizard")) { + if (Functions.getIsUnitTest()) { + return InstallState.TEST; + } + + if (Boolean.getBoolean("hudson.Main.development")) { + return InstallState.DEVELOPMENT; + } } VersionNumber lastRunVersion = new VersionNumber(getLastExecVersion()); diff --git a/core/src/main/java/jenkins/install/SetupWizard.java b/core/src/main/java/jenkins/install/SetupWizard.java new file mode 100644 index 0000000000..a6bde8d883 --- /dev/null +++ b/core/src/main/java/jenkins/install/SetupWizard.java @@ -0,0 +1,180 @@ +package jenkins.install; + +import java.io.IOException; +import java.util.Locale; +import java.util.UUID; +import java.util.logging.Logger; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +import hudson.BulkChange; +import hudson.ExtensionList; +import hudson.model.User; +import hudson.model.UserProperty; +import hudson.security.AuthorizationStrategy; +import hudson.security.FullControlOnceLoggedInAuthorizationStrategy; +import hudson.security.HudsonPrivateSecurityRealm; +import hudson.security.PermissionAdder; +import hudson.security.SecurityRealm; +import hudson.security.csrf.DefaultCrumbIssuer; +import hudson.util.PluginServletFilter; +import jenkins.model.Jenkins; +import jenkins.security.s2m.AdminWhitelistRule; + +/** + * A Jenkins instance used during first-run to provide a limited set of services while + * initial installation is in progress + */ +public class SetupWizard { + /** + * The security token parameter name + */ + public static String initialSetupAdminUserName = "initial-setup-admin-user"; + + private final Logger LOGGER = Logger.getLogger(SetupWizard.class.getName()); + + public SetupWizard(Jenkins j) throws IOException { + User admin; + // Create an admin user by default with a + // difficult password + if(j.getSecurityRealm() == null || j.getSecurityRealm() == SecurityRealm.NO_AUTHENTICATION) { // this seems very fragile + BulkChange bc = new BulkChange(j); + + HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null); + j.setSecurityRealm(securityRealm); + String randomUUID = UUID.randomUUID().toString().replace("-", "").toLowerCase(Locale.ENGLISH); + admin = securityRealm.createAccount(SetupWizard.initialSetupAdminUserName, randomUUID); + admin.addProperty(new SetupWizard.AuthenticationKey(randomUUID)); + + AuthorizationStrategy as = Jenkins.getInstance().getAuthorizationStrategy(); + for (PermissionAdder adder : ExtensionList.lookup(PermissionAdder.class)) { + if (adder.add(as, admin, Jenkins.ADMINISTER)) { + return; + } + } + + // Lock Jenkins down: + FullControlOnceLoggedInAuthorizationStrategy authStrategy = new FullControlOnceLoggedInAuthorizationStrategy(); + authStrategy.setAllowAnonymousRead(false); + j.setAuthorizationStrategy(authStrategy); + + // Shut down all the ports we can by default: + j.setSlaveAgentPort(-1); // -1 to disable + + // require a crumb issuer + j.setCrumbIssuer(new DefaultCrumbIssuer(false)); + + // set master -> slave security: + j.getInjector().getInstance(AdminWhitelistRule.class) + .setMasterKillSwitch(false); + + try{ + j.save(); // !! + } finally { + bc.commit(); + } + } + else { + admin = j.getUser(SetupWizard.initialSetupAdminUserName); + } + + String setupKey = null; + if(admin != null && admin.getProperty(SetupWizard.AuthenticationKey.class) != null) { + setupKey = admin.getProperty(SetupWizard.AuthenticationKey.class).getKey(); + } + if(setupKey != null) { + LOGGER.info("\n\n*************************************************************\n" + + "*************************************************************\n" + + "*************************************************************\n" + + "\n" + + "Jenkins initial setup is required. A security token is required to proceed. \n" + + "Please use the following security token to proceed to installation: \n" + + "\n" + + "" + setupKey + "\n" + + "\n" + + "*************************************************************\n" + + "*************************************************************\n" + + "*************************************************************\n"); + } + + try { + PluginServletFilter.addFilter(FORCE_SETUP_WIZARD_FILTER); + } catch (ServletException e) { + throw new AssertionError(e); + } + } + + /** + * Remove the setupWizard filter, ensure all updates are written to disk, etc + */ + public void doCompleteSetupWizard() { + InstallUtil.saveLastExecVersion(); + try { + PluginServletFilter.removeFilter(FORCE_SETUP_WIZARD_FILTER); + } catch (ServletException e) { + throw new AssertionError(e); // never happen because our Filter.init is no-op + } + } + + // Stores a user property for the authentication key, which is really the auto-generated user's password + public static class AuthenticationKey extends UserProperty { + String key; + + public AuthenticationKey() { + } + + public AuthenticationKey(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + } + + /** + * This filter will validate that the security token is provided + */ + private final Filter FORCE_SETUP_WIZARD_FILTER = new Filter() { + @Override + public void init(FilterConfig cfg) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + // As an extra measure of security, the install wizard generates a security token, and + // requires the user to enter it before proceeding through the installation. Once set + // we'll set a cookie so the subsequent operations succeed + if (request instanceof HttpServletRequest) { + HttpServletRequest req = (HttpServletRequest)request; + //if (!Pattern.compile(".*[.](css|ttf|gif|woff|eot|png|js)").matcher(req.getRequestURI()).matches()) { + // Allow js & css requests through + if((req.getContextPath() + "/").equals(req.getRequestURI())) { + chain.doFilter(new HttpServletRequestWrapper(req) { + public String getRequestURI() { + return getContextPath() + "/setupWizard/"; + } + }, response); + return; + } + // fall through to handling the request normally + } + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + }; +} diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 60154b64fe..e6a96e9d23 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -194,6 +194,7 @@ import jenkins.ExtensionRefreshException; import jenkins.InitReactorRunner; import jenkins.install.InstallState; import jenkins.install.InstallUtil; +import jenkins.install.SetupWizard; import jenkins.model.ProjectNamingStrategy.DefaultProjectNamingStrategy; import jenkins.security.ConfidentialKey; import jenkins.security.ConfidentialStore; @@ -333,7 +334,13 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve /** * The Jenkins instance startup type i.e. NEW, UPGRADE etc */ - private InstallState installState; + private transient InstallState installState = InstallState.NEW; + + /** + * If we're in the process of an initial setup, + * this will be set + */ + private transient SetupWizard setupWizard; /** * Number of executors of the master node. @@ -759,7 +766,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve protected Jenkins(File root, ServletContext context, PluginManager pluginManager) throws IOException, InterruptedException, ReactorException { long start = System.currentTimeMillis(); - // As Jenkins is starting, grant this process full control + // As Jenkins is starting, grant this process full control ACL.impersonate(ACL.SYSTEM); try { this.root = root; @@ -773,7 +780,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if (installState == InstallState.RESTART || installState == InstallState.DOWNGRADE) { InstallUtil.saveLastExecVersion(); } - + if (!new File(root,"jobs").exists()) { // if this is a fresh install, use more modern default layout that's consistent with agents workspaceDir = "${JENKINS_HOME}/workspace/${ITEM_FULLNAME}"; @@ -834,6 +841,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve if(KILL_AFTER_LOAD) System.exit(0); + if(!installState.isSetupComplete()) { + // Start immediately with the setup wizard for new installs + setupWizard = new SetupWizard(this); + } + launchTcpSlaveAgentListener(); if (UDPBroadcastThread.PORT != -1) { @@ -913,6 +925,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve * Get the Jenkins {@link jenkins.install.InstallState install state}. * @return The Jenkins {@link jenkins.install.InstallState install state}. */ + @Nonnull @Restricted(NoExternalUse.class) public InstallState getInstallState() { return installState; @@ -923,7 +936,12 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @Restricted(NoExternalUse.class) public void setInstallState(@Nonnull InstallState newState) { + InstallState prior = installState; installState = newState; + if(setupWizard != null && !newState.equals(prior) && newState.isSetupComplete()) { + setupWizard.doCompleteSetupWizard(); + setupWizard = null; + } } /** @@ -1437,10 +1455,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @Exported(name="jobs") public List getItems() { - if (authorizationStrategy instanceof AuthorizationStrategy.Unsecured || - authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) { - return new ArrayList(items.values()); - } + if (authorizationStrategy instanceof AuthorizationStrategy.Unsecured || + authorizationStrategy instanceof FullControlOnceLoggedInAuthorizationStrategy) { + return new ArrayList(items.values()); + } List viewableItems = new ArrayList(); for (TopLevelItem item : items.values()) { @@ -1815,11 +1833,11 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } public DescribableList, NodePropertyDescriptor> getNodeProperties() { - return nodeProperties; + return nodeProperties; } public DescribableList, NodePropertyDescriptor> getGlobalNodeProperties() { - return globalNodeProperties; + return globalNodeProperties; } /** @@ -2428,7 +2446,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve */ @Override public TopLevelItem getItem(String name) throws AccessDeniedException { if (name==null) return null; - TopLevelItem item = items.get(name); + TopLevelItem item = items.get(name); if (item==null) return null; if (!item.hasPermission(Item.READ)) { @@ -3079,10 +3097,10 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } public HttpResponse doToggleCollapse() throws ServletException, IOException { - final StaplerRequest request = Stapler.getCurrentRequest(); - final String paneId = request.getParameter("paneId"); + final StaplerRequest request = Stapler.getCurrentRequest(); + final String paneId = request.getParameter("paneId"); - PaneStatusProperties.forCurrentUser().toggleCollapsed(paneId); + PaneStatusProperties.forCurrentUser().toggleCollapsed(paneId); return HttpResponses.forwardToPreviousPage(); } @@ -3113,9 +3131,9 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve LOGGER.info("Failed to get thread dump for node " + c.getName() + ": " + e.getMessage()); } } - if (toComputer() == null) { - future.put("master", RemotingDiagnostics.getThreadDumpAsync(FilePath.localChannel)); - } + if (toComputer() == null) { + future.put("master", RemotingDiagnostics.getThreadDumpAsync(FilePath.localChannel)); + } // if the result isn't available in 5 sec, ignore that. // this is a precaution against hang nodes @@ -3891,6 +3909,13 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve public List getManagementLinks() { return ManagementLink.all(); } + + /** + * If set, a currently active setup wizard - e.g. installation + */ + public SetupWizard getSetupWizard() { + return setupWizard; + } /** * Exposes the current user to /me URL. diff --git a/core/src/main/resources/hudson/security/FullControlOnceLoggedInAuthorizationStrategy/config.jelly b/core/src/main/resources/hudson/security/FullControlOnceLoggedInAuthorizationStrategy/config.jelly new file mode 100644 index 0000000000..0e627effbc --- /dev/null +++ b/core/src/main/resources/hudson/security/FullControlOnceLoggedInAuthorizationStrategy/config.jelly @@ -0,0 +1,7 @@ + + + + + + diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly index bb12a30aa9..346d8c4d53 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryForm.jelly @@ -25,64 +25,43 @@ THE SOFTWARE. - - - - - - - - -

${title}

-
- -
- ${data.errorMessage} -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
${%Username}:
${%Password}:
${%Confirm password}:
${%Full name}:
${%E-mail address}:
${%Enter text as shown}: -
- [captcha] -
- - - +

${title}

+
+ +
+ ${data.errorMessage}
- - +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
${%Username}:
${%Password}:
${%Confirm password}:
${%Full name}:
${%E-mail address}:
${%Enter text as shown}: +
+ [captcha] +
+
diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryFormPage.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryFormPage.jelly new file mode 100644 index 0000000000..8009ad6518 --- /dev/null +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/_entryFormPage.jelly @@ -0,0 +1,50 @@ + + + + + + + + + + + + + +
+ + + + +
+
+
diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly index aed3d5c5c8..daacc17bfc 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/addUser.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + \ No newline at end of file diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly index bc44b42acc..6ebd369ae0 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/firstUser.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + \ No newline at end of file diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/setupWizardFirstUser.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/setupWizardFirstUser.jelly new file mode 100644 index 0000000000..4f7475462f --- /dev/null +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/setupWizardFirstUser.jelly @@ -0,0 +1,53 @@ + + + + + +
+ + + +
+
diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly index 656e96c3c9..f2477da1df 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signup.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + diff --git a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly index 01c0197a62..8daec8d381 100644 --- a/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly +++ b/core/src/main/resources/hudson/security/HudsonPrivateSecurityRealm/signupWithFederatedIdentity.jelly @@ -27,5 +27,5 @@ THE SOFTWARE. --> - + \ No newline at end of file diff --git a/core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly b/core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly new file mode 100644 index 0000000000..b22936d9ad --- /dev/null +++ b/core/src/main/resources/jenkins/install/SetupWizard/authenticate-security-token.jelly @@ -0,0 +1,44 @@ + + + +
+
+ +
+
+
+
diff --git a/core/src/main/resources/jenkins/install/SetupWizard/index.jelly b/core/src/main/resources/jenkins/install/SetupWizard/index.jelly new file mode 100644 index 0000000000..d001d433d5 --- /dev/null +++ b/core/src/main/resources/jenkins/install/SetupWizard/index.jelly @@ -0,0 +1,8 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +