/******************************************************************************* * * Copyright (c) 2011-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: * * Kohsuke Kawaguchi, Nikita Levyankov, Winston Prakash * *******************************************************************************/ package hudson.security; import hudson.EnvVars; import hudson.FilePath; import hudson.cli.CLICommand; import hudson.model.Hudson; import hudson.remoting.Callable; import hudson.tasks.MailAddressResolver; import java.io.Console; import java.io.IOException; import java.util.Arrays; import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder; import org.kohsuke.args4j.Option; import org.springframework.dao.DataAccessException; import org.springframework.security.authentication.AnonymousAuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.RememberMeAuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; /** * Partial implementation of {@link SecurityRealm} for username/password based * authentication. This is a convenience base class if all you are trying to do * is to check the given username and password with the information stored in * somewhere else, and you don't want to do anything with Spring Security. * <p/> * < * p/> * This {@link SecurityRealm} uses the standard login form (and a few other * optional mechanisms like BASIC auth) to gather the username/password * information. Subtypes are responsible for authenticating this information. * * @author Kohsuke Kawaguchi * @author Nikita Levyankov * @since 1.317 */ public abstract class AbstractPasswordBasedSecurityRealm extends SecurityRealm implements UserDetailsService { @Override public SecurityComponents createSecurityComponents() { // this does all the hard work Authenticator authenticator = new Authenticator(); // these providers apply everywhere RememberMeAuthenticationProvider rememberMeAuthenticationProvider = new RememberMeAuthenticationProvider(); rememberMeAuthenticationProvider.setKey(HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getSecretKey()); // this doesn't mean we allow anonymous access. // we just authenticate anonymous users as such, // so that later authorization can reject them if so configured AnonymousAuthenticationProvider anonymousAuthenticationProvider = new AnonymousAuthenticationProvider(); anonymousAuthenticationProvider.setKey("anonymous"); AuthenticationProvider[] authenticationProvider = { authenticator, rememberMeAuthenticationProvider, anonymousAuthenticationProvider }; ProviderManager providerManager = new ProviderManager(); providerManager.setProviders(Arrays.asList(authenticationProvider)); return new SecurityComponents(providerManager, this); } @Override public CliAuthenticator createCliAuthenticator(final CLICommand command) { return new CliAuthenticator() { @Option(name = "--username", usage = "User name to authenticate yourself to Hudson") public String userName; @Option(name = "--password", usage = "Password for authentication. Note that passing a password in arguments is insecure.") public String password; @Option(name = "--password-file", usage = "File that contains the password") public String passwordFile; public Authentication authenticate() throws AuthenticationException, IOException, InterruptedException { if (userName == null) { return Hudson.ANONYMOUS; // no authentication parameter. run as anonymous } if (passwordFile != null) { try { password = new FilePath(command.channel, passwordFile).readToString().trim(); } catch (IOException e) { throw new BadCredentialsException("Failed to read " + passwordFile, e); } } if (password == null) { password = command.channel.call(new InteractivelyAskForPassword()); } if (password == null) { throw new BadCredentialsException("No password specified"); } UserDetails d = AbstractPasswordBasedSecurityRealm.this.doAuthenticate(userName, password); return new UsernamePasswordAuthenticationToken(d, password, d.getAuthorities()); } }; } /** * Authenticate a login attempt. This method is the heart of a * {@link AbstractPasswordBasedSecurityRealm}. * <p/> * < * p/> * If the user name and the password pair matches, retrieve the information * about this user and return it as a {@link UserDetails} object. * {@link org.springframework.security.userdetails.User} is a convenient * implementation to use, but if your backend offers additional data, you * may want to use your own subtype so that the rest of Hudson can use those * additional information (such as e-mail address --- see * {@link MailAddressResolver}.) * <p/> * < * p/> * Properties like {@link UserDetails#getPassword()} make no sense, so just * return an empty value from it. The only information that you need to pay * real attention is {@link UserDetails#getAuthorities()}, which is a list * of roles/groups that the user is in. At minimum, this must contain * {@link #AUTHENTICATED_AUTHORITY} (which indicates that this user is * authenticated and not anonymous), but if your backend supports a notion * of groups, you should make sure that the authorities contain one entry * per one group. This enables users to control authorization based on * groups. * <p/> * < * p/> * If the user name and the password pair doesn't match, throw * {@link AuthenticationException} to reject the login attempt. If * authentication was successful - HUDSON_USER environment variable will be * set <a * href='http://issues.hudson-ci.org/browse/HUDSON-4463'>HUDSON-4463</a> */ protected UserDetails doAuthenticate(String username, String password) throws AuthenticationException { UserDetails userDetails = authenticate(username, password); EnvVars.setHudsonUserEnvVar(userDetails.getUsername()); return userDetails; } /** * Implements same logic as {@link #doAuthenticate(String, String)} method, * but doesn't set hudson_user env variable. */ protected abstract UserDetails authenticate(String username, String password) throws AuthenticationException; /** * Retrieves information about an user by its name. * <p/> * < * p/> * This method is used, for example, to validate if the given token is a * valid user name when the user is configuring an ACL. This is an optional * method that improves the user experience. If your backend doesn't support * a query like this, just always throw {@link UsernameNotFoundException}. */ @Override public abstract UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException; /** * Retrieves information about a group by its name. * <p/> * This method is the group version of the * {@link #loadUserByUsername(String)}. */ @Override public abstract GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException; class Authenticator extends AbstractUserDetailsAuthenticationProvider { protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // authentication is assumed to be done already in the retrieveUser method } protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { return AbstractPasswordBasedSecurityRealm.this.doAuthenticate(username, authentication.getCredentials().toString()); } } /** * Asks for the password. */ private static class InteractivelyAskForPassword implements Callable<String, IOException> { public String call() throws IOException { Console console = System.console(); if (console == null) { return null; // no terminal } char[] w = console.readPassword("Password:"); if (w == null) { return null; } return new String(w); } private static final long serialVersionUID = 1L; } }