package jenkins.install;
import static org.apache.commons.io.FileUtils.readFileToString;
import static org.apache.commons.lang.StringUtils.defaultIfBlank;
import java.io.IOException;
import java.util.Locale;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
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 javax.servlet.http.HttpServletResponse;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import hudson.BulkChange;
import hudson.Extension;
import hudson.FilePath;
import hudson.ProxyConfiguration;
import hudson.model.PageDecorator;
import hudson.model.UpdateCenter;
import hudson.model.UpdateSite;
import hudson.model.User;
import hudson.security.FullControlOnceLoggedInAuthorizationStrategy;
import hudson.security.HudsonPrivateSecurityRealm;
import hudson.security.SecurityRealm;
import hudson.security.csrf.DefaultCrumbIssuer;
import hudson.util.HttpResponses;
import hudson.util.PluginServletFilter;
import hudson.util.VersionNumber;
import java.io.File;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import java.util.List;
import jenkins.model.Jenkins;
import jenkins.security.s2m.AdminWhitelistRule;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.kohsuke.accmod.restrictions.DoNotUse;
/**
* A Jenkins instance used during first-run to provide a limited set of services while
* initial installation is in progress
*
* @since 2.0
*/
@Restricted(NoExternalUse.class)
@Extension
public class SetupWizard extends PageDecorator {
/**
* The security token parameter name
*/
public static String initialSetupAdminUserName = "admin";
private static final Logger LOGGER = Logger.getLogger(SetupWizard.class.getName());
/**
* Used to determine if this was a new install (vs. an upgrade, restart, or otherwise)
*/
private static boolean isUsingSecurityToken = false;
/**
* Initialize the setup wizard, this will process any current state initializations
*/
/*package*/ void init(boolean newInstall) throws IOException, InterruptedException {
Jenkins jenkins = Jenkins.getInstance();
if(newInstall) {
// this was determined to be a new install, don't run the update wizard here
setCurrentLevel(Jenkins.getVersion());
// Create an admin user by default with a
// difficult password
FilePath iapf = getInitialAdminPasswordFile();
if(jenkins.getSecurityRealm() == null || jenkins.getSecurityRealm() == SecurityRealm.NO_AUTHENTICATION) { // this seems very fragile
try (BulkChange bc = new BulkChange(jenkins)) {
HudsonPrivateSecurityRealm securityRealm = new HudsonPrivateSecurityRealm(false, false, null);
jenkins.setSecurityRealm(securityRealm);
String randomUUID = UUID.randomUUID().toString().replace("-", "").toLowerCase(Locale.ENGLISH);
// create an admin user
securityRealm.createAccount(SetupWizard.initialSetupAdminUserName, randomUUID);
// JENKINS-33599 - write to a file in the jenkins home directory
// most native packages of Jenkins creates a machine user account 'jenkins' to run Jenkins,
// and use group 'jenkins' for admins. So we allow groups to read this file
iapf.touch(System.currentTimeMillis());
iapf.chmod(0640);
iapf.write(randomUUID + System.lineSeparator(), "UTF-8");
// Lock Jenkins down:
FullControlOnceLoggedInAuthorizationStrategy authStrategy = new FullControlOnceLoggedInAuthorizationStrategy();
authStrategy.setAllowAnonymousRead(false);
jenkins.setAuthorizationStrategy(authStrategy);
// Shut down all the ports we can by default:
jenkins.setSlaveAgentPort(-1); // -1 to disable
// require a crumb issuer
jenkins.setCrumbIssuer(new DefaultCrumbIssuer(false));
// set master -> slave security:
jenkins.getInjector().getInstance(AdminWhitelistRule.class)
.setMasterKillSwitch(false);
jenkins.save(); // !!
bc.commit();
}
}
if(iapf.exists()) {
String setupKey = iapf.readToString().trim();
String ls = System.lineSeparator();
LOGGER.info(ls + ls + "*************************************************************" + ls
+ "*************************************************************" + ls
+ "*************************************************************" + ls
+ ls
+ "Jenkins initial setup is required. An admin user has been created and "
+ "a password generated." + ls
+ "Please use the following password to proceed to installation:" + ls
+ ls
+ setupKey + ls
+ ls
+ "This may also be found at: " + iapf.getRemote() + ls
+ ls
+ "*************************************************************" + ls
+ "*************************************************************" + ls
+ "*************************************************************" + ls);
}
try {
PluginServletFilter.addFilter(FORCE_SETUP_WIZARD_FILTER);
// if we're not using security defaults, we should not show the security token screen
// users will likely be sent to a login screen instead
isUsingSecurityToken = isUsingSecurityDefaults();
} catch (ServletException e) {
throw new RuntimeException("Unable to add PluginServletFilter for the SetupWizard", e);
}
}
try {
// Make sure plugin metadata is up to date
UpdateCenter.updateDefaultSite();
} catch (Exception e) {
LOGGER.log(Level.WARNING, e.getMessage(), e);
}
}
/**
* Indicates a generated password should be used - e.g. this is a new install, no security realm set up
*/
public boolean isUsingSecurityToken() {
try {
return isUsingSecurityToken // only ever show the unlock page if using the security token
&& !Jenkins.getInstance().getInstallState().isSetupComplete()
&& isUsingSecurityDefaults();
} catch (Exception e) {
// ignore
}
return false;
}
/**
* Determines if the security settings seem to match the defaults. Here, we only
* really care about and test for HudsonPrivateSecurityRealm and the user setup.
* Other settings are irrelevant.
*/
/*package*/ boolean isUsingSecurityDefaults() {
Jenkins j = Jenkins.getInstance();
if (j.getSecurityRealm() instanceof HudsonPrivateSecurityRealm) {
HudsonPrivateSecurityRealm securityRealm = (HudsonPrivateSecurityRealm)j.getSecurityRealm();
try {
if(securityRealm.getAllUsers().size() == 1) {
HudsonPrivateSecurityRealm.Details details = securityRealm.loadUserByUsername(SetupWizard.initialSetupAdminUserName);
FilePath iapf = getInitialAdminPasswordFile();
if (iapf.exists()) {
if (details.isPasswordCorrect(iapf.readToString().trim())) {
return true;
}
}
}
} catch(UsernameNotFoundException | IOException | InterruptedException e) {
return false; // Not initial security setup if no transitional admin user / password found
}
}
return false;
}
/**
* Called during the initial setup to create an admin user
*/
public void doCreateAdminUser(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
Jenkins j = Jenkins.getInstance();
j.checkPermission(Jenkins.ADMINISTER);
// This will be set up by default. if not, something changed, ok to fail
HudsonPrivateSecurityRealm securityRealm = (HudsonPrivateSecurityRealm)j.getSecurityRealm();
User admin = securityRealm.getUser(SetupWizard.initialSetupAdminUserName);
try {
if(admin != null) {
admin.delete(); // assume the new user may well be 'admin'
}
User u = securityRealm.createAccountByAdmin(req, rsp, "/jenkins/install/SetupWizard/setupWizardFirstUser.jelly", req.getContextPath() + "/");
if (u != null) {
if(admin != null) {
admin = null;
}
// Success! Delete the temporary password file:
try {
getInitialAdminPasswordFile().delete();
} catch (InterruptedException e) {
throw new IOException(e);
}
InstallUtil.proceedToNextStateFrom(InstallState.CREATE_ADMIN_USER);
// ... and then login
Authentication a = new UsernamePasswordAuthenticationToken(u.getId(),req.getParameter("password1"));
a = securityRealm.getSecurityComponents().manager.authenticate(a);
SecurityContextHolder.getContext().setAuthentication(a);
}
} finally {
if(admin != null) {
admin.save(); // recreate this initial user if something failed
}
}
}
/*package*/ void setCurrentLevel(VersionNumber v) throws IOException {
FileUtils.writeStringToFile(getUpdateStateFile(), v.toString());
}
/**
* File that captures the state of upgrade.
*
* This file records the version number that the installation has upgraded to.
*/
/*package*/ static File getUpdateStateFile() {
return new File(Jenkins.getInstance().getRootDir(),"jenkins.install.UpgradeWizard.state");
}
/**
* What is the version the upgrade wizard has run the last time and upgraded to?.
* If {@link #getUpdateStateFile()} is missing, presumes the baseline is 1.0
* @return Current baseline. {@code null} if it cannot be retrieved.
*/
@Restricted(NoExternalUse.class)
@CheckForNull
public VersionNumber getCurrentLevel() {
VersionNumber from = new VersionNumber("1.0");
File state = getUpdateStateFile();
if (state.exists()) {
try {
from = new VersionNumber(defaultIfBlank(readFileToString(state), "1.0").trim());
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Cannot read the current version file", ex);
return null;
}
}
return from;
}
/**
* Returns the initial plugin list in JSON format
*/
@Restricted(DoNotUse.class) // WebOnly
public HttpResponse doPlatformPluginList() throws IOException {
jenkins.install.SetupWizard setupWizard = Jenkins.getInstance().getSetupWizard();
if (setupWizard != null) {
if (InstallState.UPGRADE.equals(Jenkins.getInstance().getInstallState())) {
JSONArray initialPluginData = getPlatformPluginUpdates();
if(initialPluginData != null) {
return HttpResponses.okJSON(initialPluginData);
}
} else {
JSONArray initialPluginData = getPlatformPluginList();
if(initialPluginData != null) {
return HttpResponses.okJSON(initialPluginData);
}
}
}
return HttpResponses.okJSON();
}
/**
* Provides the list of platform plugin updates from the last time
* the upgrade was run.
* @return {@code null} if the version range cannot be retrieved.
*/
@CheckForNull
public JSONArray getPlatformPluginUpdates() {
final VersionNumber version = getCurrentLevel();
if (version == null) {
return null;
}
return getPlatformPluginsForUpdate(version, Jenkins.getVersion());
}
/**
* Gets the suggested plugin list from the update sites, falling back to a local version
* @return JSON array with the categorized plugon list
*/
@CheckForNull
/*package*/ JSONArray getPlatformPluginList() {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
JSONArray initialPluginList = null;
updateSiteList: for (UpdateSite updateSite : Jenkins.getInstance().getUpdateCenter().getSiteList()) {
String updateCenterJsonUrl = updateSite.getUrl();
String suggestedPluginUrl = updateCenterJsonUrl.replace("/update-center.json", "/platform-plugins.json");
try {
URLConnection connection = ProxyConfiguration.open(new URL(suggestedPluginUrl));
try {
if(connection instanceof HttpURLConnection) {
int responseCode = ((HttpURLConnection)connection).getResponseCode();
if(HttpURLConnection.HTTP_OK != responseCode) {
throw new HttpRetryException("Invalid response code (" + responseCode + ") from URL: " + suggestedPluginUrl, responseCode);
}
}
String initialPluginJson = IOUtils.toString(connection.getInputStream(), "utf-8");
initialPluginList = JSONArray.fromObject(initialPluginJson);
break updateSiteList;
} catch(Exception e) {
// not found or otherwise unavailable
LOGGER.log(Level.FINE, e.getMessage(), e);
continue updateSiteList;
}
} catch(Exception e) {
LOGGER.log(Level.FINE, e.getMessage(), e);
}
}
if (initialPluginList == null) {
// fall back to local file
try {
ClassLoader cl = getClass().getClassLoader();
URL localPluginData = cl.getResource("jenkins/install/platform-plugins.json");
String initialPluginJson = IOUtils.toString(localPluginData.openStream(), "utf-8");
initialPluginList = JSONArray.fromObject(initialPluginJson);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
return initialPluginList;
}
/**
* Get the platform plugins added in the version range
*/
/*package*/ JSONArray getPlatformPluginsForUpdate(VersionNumber from, VersionNumber to) {
Jenkins jenkins = Jenkins.getInstance();
JSONArray pluginCategories = JSONArray.fromObject(getPlatformPluginList().toString());
for (Iterator<?> categoryIterator = pluginCategories.iterator(); categoryIterator.hasNext();) {
Object category = categoryIterator.next();
if (category instanceof JSONObject) {
JSONObject cat = (JSONObject)category;
JSONArray plugins = cat.getJSONArray("plugins");
nextPlugin: for (Iterator<?> pluginIterator = plugins.iterator(); pluginIterator.hasNext();) {
Object pluginData = pluginIterator.next();
if (pluginData instanceof JSONObject) {
JSONObject plugin = (JSONObject)pluginData;
if (plugin.has("added")) {
String sinceVersion = plugin.getString("added");
if (sinceVersion != null) {
VersionNumber v = new VersionNumber(sinceVersion);
if(v.compareTo(to) <= 0 && v.compareTo(from) > 0) {
// This plugin is valid, we'll leave "suggested" state
// to match the experience during install
// but only add it if it's currently uninstalled
String pluginName = plugin.getString("name");
if (null == jenkins.getPluginManager().getPlugin(pluginName)) {
// Also check that a compatible version exists in an update site
boolean foundCompatibleVersion = false;
for (UpdateSite site : jenkins.getUpdateCenter().getSiteList()) {
UpdateSite.Plugin sitePlug = site.getPlugin(pluginName);
if (sitePlug != null
&& !sitePlug.isForNewerHudson()
&& !sitePlug.isNeededDependenciesForNewerJenkins()) {
foundCompatibleVersion = true;
break;
}
}
if (foundCompatibleVersion) {
continue nextPlugin;
}
}
}
}
}
}
pluginIterator.remove();
}
if (plugins.isEmpty()) {
categoryIterator.remove();
}
}
}
return pluginCategories;
}
/**
* Gets the file used to store the initial admin password
*/
public FilePath getInitialAdminPasswordFile() {
return Jenkins.getInstance().getRootPath().child("secrets/initialAdminPassword");
}
/**
* Remove the setupWizard filter, ensure all updates are written to disk, etc
*/
public HttpResponse doCompleteInstall() throws IOException, ServletException {
completeSetup();
return HttpResponses.okJSON();
}
/*package*/ void completeSetup() throws IOException, ServletException {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
InstallUtil.saveLastExecVersion();
setCurrentLevel(Jenkins.getVersion());
PluginServletFilter.removeFilter(FORCE_SETUP_WIZARD_FILTER);
isUsingSecurityToken = false; // this should not be considered new anymore
InstallUtil.proceedToNextStateFrom(InstallState.INITIAL_SETUP_COMPLETED);
}
/**
* Gets all the install states
*/
public List<InstallState> getInstallStates() {
return InstallState.all();
}
/**
* Returns an installState by name
*/
public InstallState getInstallState(String name) {
if (name == null) {
return null;
}
return InstallState.valueOf(name);
}
/**
* 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 {
// Force root requests to the setup wizard
if (request instanceof HttpServletRequest) {
HttpServletRequest req = (HttpServletRequest) request;
String requestURI = req.getRequestURI();
if (requestURI.equals(req.getContextPath()) && !requestURI.endsWith("/")) {
((HttpServletResponse) response).sendRedirect(req.getContextPath() + "/");
return;
} else if (req.getRequestURI().equals(req.getContextPath() + "/")) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
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() {
}
};
}