/**
* *****************************************************************************
*
* Copyright (c) 2012 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Winston Prakash
*
******************************************************************************
*/
package org.eclipse.hudson.security;
import com.thoughtworks.xstream.XStream;
import hudson.BulkChange;
import hudson.Functions;
import hudson.Util;
import hudson.XmlFile;
import hudson.markup.MarkupFormatter;
import hudson.markup.RawHtmlMarkupFormatter;
import hudson.model.Descriptor.FormException;
import hudson.model.Hudson;
import hudson.model.Saveable;
import hudson.model.listeners.SaveableListener;
import hudson.security.*;
import hudson.util.TextFile;
import hudson.util.XStream2;
import hudson.util.XmlUtils;
import java.io.File;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.SecretKey;
import javax.servlet.ServletException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import net.sf.json.JSONObject;
import org.eclipse.hudson.security.team.TeamManager;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.context.SecurityContextHolder;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Manager that manages Hudson Security. The configuration is written to the
* file hudson-security.xml
*
* @author Winston Prakash
* @since 3.0.0
*/
public class HudsonSecurityManager implements Saveable {
private transient final String securityConfigFileName = "hudson-security.xml";
private transient Logger logger = LoggerFactory.getLogger(HudsonSecurityManager.class);
/**
* {@link Authentication} object that represents the anonymous user. Because
* Spring Security creates its own {@link AnonymousAuthenticationToken}
* instances, the code must not expect the singleton semantics. This is just
* a convenient instance.
*
* @since 1.343
*/
public static final Authentication ANONYMOUS = new AnonymousAuthenticationToken(
"anonymous", "anonymous", Arrays.asList(new GrantedAuthority[]{new GrantedAuthorityImpl("anonymous")}));
/**
* Controls a part of the <a
* href="http://en.wikipedia.org/wiki/Authentication">authentication</a>
* handling in Hudson.
* <p>
* Intuitively, this corresponds to the user database.
*
* See {@link HudsonFilter} for the concrete authentication protocol.
*
* Never null. Always use {@link #setSecurityRealm(SecurityRealm)} to update
* this field.
*
* @see #getSecurity()
* @see #setSecurityRealm(SecurityRealm)
*/
private volatile SecurityRealm securityRealm = SecurityRealm.NO_AUTHENTICATION;
/**
* Controls how the <a
* href="http://en.wikipedia.org/wiki/Authorization">authorization</a> is
* handled in Hudson.
* <p>
* This ultimately controls who has access to what.
*
* Never null.
*/
private volatile AuthorizationStrategy authorizationStrategy = AuthorizationStrategy.UNSECURED;
/**
* False to enable anyone to do anything. Left as a field so that we can
* still read old data that uses this flag.
*
* @see #authorizationStrategy
* @see #securityRealm
*/
private Boolean useSecurity;
private MarkupFormatter markupFormatter = RawHtmlMarkupFormatter.INSTANCE;
private transient File hudsonHome;
/**
* TCP slave agent port. 0 for random, -1 to disable.
*/
private int slaveAgentPort = 0;
/**
* Secrete key generated once and used for a long time, beyond container
* start/stop. Persisted outside <tt>config.xml</tt> to avoid accidental
* exposure.
*/
private transient final String secretKey;
private transient final TeamManager teamManager;
public HudsonSecurityManager(File hudsonHome) throws IOException {
this.hudsonHome = hudsonHome;
teamManager = new TeamManager(hudsonHome);
// get or create the secret
TextFile secretFile = new TextFile(new File(hudsonHome, "secret.key"));
if (secretFile.exists()) {
secretKey = secretFile.readTrim();
} else {
SecureRandom sr = new SecureRandom();
byte[] random = new byte[32];
sr.nextBytes(random);
secretKey = Util.toHexString(random);
secretFile.write(secretKey);
}
load();
}
@Exported
public int getSlaveAgentPort() {
return slaveAgentPort;
}
/**
* Get the directory where hudson stores the User configuration
*
* @return
*/
public File getHudsonHome() {
return hudsonHome;
}
/**
* Gets the markup formatter used in the system.
*
* @return never null.
*/
public MarkupFormatter getMarkupFormatter() {
return markupFormatter;
}
/**
* Sets the markup formatter used in the system globally.
*/
public void setMarkupFormatter(MarkupFormatter markupFormatter) {
this.markupFormatter = markupFormatter;
}
/**
* Returns the {@link ACL} for this object.
*/
public ACL getACL() {
return authorizationStrategy.getRootACL();
}
/**
* Short for {@code getACL().checkPermission(p)}
*/
public void checkPermission(Permission p) {
getACL().checkPermission(p);
}
/**
* Short for {@code getACL().hasPermission(p)}
*/
public boolean hasPermission(Permission p) {
return getACL().hasPermission(p);
}
/**
* Returns a secret key that survives across container start/stop.
* <p>
* This value is useful for implementing some of the security features.
*/
public String getSecretKey() {
return secretKey;
}
/**
* Gets {@linkplain #getSecretKey() the secret key} as a key for AES-128.
*
* @since 1.308
*/
public SecretKey getSecretKeyAsAES128() {
return Util.toAes128Key(secretKey);
}
/**
* A convenience method to check if there's some security restrictions in
* place.
*/
public boolean isUseSecurity() {
return securityRealm != SecurityRealm.NO_AUTHENTICATION || authorizationStrategy != AuthorizationStrategy.UNSECURED;
}
/**
* Returns the constant that captures the three basic security modes in
* Hudson.
*/
public SecurityMode getSecurity() {
// fix the variable so that this code works under concurrent modification to securityRealm.
SecurityRealm realm = securityRealm;
if (realm == SecurityRealm.NO_AUTHENTICATION) {
return SecurityMode.UNSECURED;
}
if (realm instanceof LegacySecurityRealm) {
return SecurityMode.LEGACY;
}
return SecurityMode.SECURED;
}
/**
* Get the configured Security Realm
*
* @return never null.
*/
public SecurityRealm getSecurityRealm() {
return securityRealm;
}
/**
* Set a Security Realm to the Manager
*
* @param securityRealm
*/
public void setSecurityRealm(SecurityRealm securityRealm) {
if (securityRealm == null) {
securityRealm = SecurityRealm.NO_AUTHENTICATION;
}
this.securityRealm = securityRealm;
// reset the filters and proxies for the new SecurityRealm
try {
HudsonFilter filter = HudsonSecurityEntitiesHolder.getHudsonSecurityFilter();
if (filter == null) {
// Fix for #3069: This filter is not necessarily initialized before the servlets.
// when HudsonFilter does come back, it'll initialize itself.
logger.debug("HudsonFilter has not yet been initialized: Can't perform security setup for now");
} else {
logger.debug("HudsonFilter has been previously initialized: Setting security up");
filter.reset(securityRealm);
logger.debug("Security is now fully set up");
}
} catch (ServletException e) {
// for binary compatibility, this method cannot throw a checked exception
throw new RuntimeException("Failed to configure filter", e) {
};
}
}
/**
* Get the configured Authorization Strategy
*
* @return never null.
*/
public AuthorizationStrategy getAuthorizationStrategy() {
return authorizationStrategy;
}
/**
* Set the Authorization Strategy to the Manager
*
* @param a
*/
public void setAuthorizationStrategy(AuthorizationStrategy authStrategy) {
if (authStrategy == null) {
authStrategy = AuthorizationStrategy.UNSECURED;
}
authorizationStrategy = authStrategy;
}
/**
* Accepts submission from the configuration page.
*/
public synchronized void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
BulkChange bc = new BulkChange(this);
try {
checkPermission(Permission.HUDSON_ADMINISTER);
JSONObject json = req.getSubmittedForm();
// keep using 'useSecurity' field as the main configuration setting
// until we get the new security implementation working
// useSecurity = null;
if (json.has("use_security")) {
useSecurity = true;
JSONObject security = json.getJSONObject("use_security");
if (security.has("markupFormatter")) {
markupFormatter = req.bindJSON(MarkupFormatter.class, security.getJSONObject("markupFormatter"));
}
{
String v = req.getParameter("slaveAgentPortType");
if (!isUseSecurity() || v == null || v.equals("random")) {
slaveAgentPort = 0;
} else if (v.equals("disable")) {
slaveAgentPort = -1;
} else {
try {
slaveAgentPort = Integer.parseInt(req.getParameter("slaveAgentPort"));
} catch (NumberFormatException e) {
throw new FormException(hudson.model.Messages.Hudson_BadPortNumber(req.getParameter("slaveAgentPort")), "slaveAgentPort");
}
}
if (Hudson.getInstance() != null) {
Hudson.getInstance().setSlaveAgentPort(slaveAgentPort);
}
setSecurityRealm(SecurityRealm.all().newInstanceFromRadioList(security, "realm"));
setAuthorizationStrategy(AuthorizationStrategy.all().newInstanceFromRadioList(security, "authorization"));
}
} else {
useSecurity = null;
setSecurityRealm(SecurityRealm.NO_AUTHENTICATION);
authorizationStrategy = AuthorizationStrategy.UNSECURED;
}
rsp.sendRedirect(Functions.getRequestRootPath(req) + '/'); // go to the top page
} finally {
bc.commit();
}
}
/**
* Perform the logout action for the current user.
*
* @param req
* @param rsp
* @throws IOException
* @throws ServletException
*/
public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
securityRealm.doLogout(req, rsp);
}
/**
* The file where the Security settings are saved.
*/
protected final XmlFile getConfigFile() {
XStream xstream = new XStream2();
xstream.alias("hudsonSecurityManager", HudsonSecurityManager.class);
return new XmlFile(xstream, new File(hudsonHome, securityConfigFileName));
}
/**
* Save the settings to the configuration file.
*/
public synchronized void save() throws IOException {
if (BulkChange.contains(this)) {
return;
}
getConfigFile().write(this);
SaveableListener.fireOnChange(this, getConfigFile());
}
/**
* Load the settings from the configuration file
*/
private void load() {
logger.info("Loading Security ..");
XmlFile config = getConfigFile();
try {
if (config.exists()) {
config.unmarshal(this);
} else {
// Compatibility. Hudson 2.x stores Security config in the Global Config file.
if (extractSecurityConfig()) {
config.unmarshal(this);
}
}
} catch (IOException e) {
logger.error("Failed to load " + config, e);
}
// read in old data that doesn't have the security field set
if (authorizationStrategy == null) {
if (useSecurity == null || !useSecurity) {
authorizationStrategy = AuthorizationStrategy.UNSECURED;
} else {
authorizationStrategy = new FullControlOnceLoggedInAuthorizationStrategy();
}
}
if (securityRealm == null) {
if (useSecurity == null || !useSecurity) {
setSecurityRealm(SecurityRealm.NO_AUTHENTICATION);
} else {
setSecurityRealm(new LegacySecurityRealm());
}
} else {
// force the set to proxy
setSecurityRealm(securityRealm);
}
if (useSecurity != null && !useSecurity) {
// forced reset to the unsecure mode.
// this works as an escape hatch for people who locked themselves out.
authorizationStrategy = AuthorizationStrategy.UNSECURED;
setSecurityRealm(SecurityRealm.NO_AUTHENTICATION);
}
}
/**
* Convenient static method to provide full control
*/
public static void grantFullControl() {
SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM);
}
public static void resetFullControl() {
SecurityContextHolder.clearContext();
}
/**
* Gets the {@link Authentication} object that represents the user
* associated with the current request.
*/
public static Authentication getAuthentication() {
Authentication a = SecurityContextHolder.getContext().getAuthentication();
// on Tomcat while serving the login page, this is null despite the fact
// that we have filters. Looking at the stack trace, Tomcat doesn't seem to
// run the request through filters when this is the login request.
// see http://www.nabble.com/Matrix-authorization-problem-tp14602081p14886312.html
if (a == null) {
a = ANONYMOUS;
}
return a;
}
private boolean extractSecurityConfig() {
try {
File globalConfigFile = new File(hudsonHome, "config.xml");
if (!globalConfigFile.exists()) {
return false;
}
Document globalConfigDoc = XmlUtils.parseXmlFile(globalConfigFile);
if (XmlUtils.hasElement(globalConfigDoc, "useSecurity")) {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document securityConfigDoc = builder.newDocument();
Element root = securityConfigDoc.createElement("hudsonSecurityManager");
securityConfigDoc.appendChild(root);
XmlUtils.moveElement(globalConfigDoc, securityConfigDoc, root, "useSecurity");
XmlUtils.moveElement(globalConfigDoc, securityConfigDoc, root, "markupFormatter");
XmlUtils.moveElement(globalConfigDoc, securityConfigDoc, root, "authorizationStrategy");
XmlUtils.moveElement(globalConfigDoc, securityConfigDoc, root, "securityRealm");
File securityConfigFile = new File(hudsonHome, securityConfigFileName);
securityConfigFile.createNewFile();
XmlUtils.writeXmlFile(securityConfigDoc, securityConfigFile);
XmlUtils.writeXmlFile(globalConfigDoc, globalConfigFile);
return true;
} else {
return false;
}
} catch (Exception exc) {
exc.printStackTrace();
return false;
}
}
public TeamManager getTeamManager() {
return teamManager;
}
}