/hudson-core/src/main/java/hudson/security/SecurityRealm.java

http://github.com/hudson/hudson · Java · 557 lines · 218 code · 46 blank · 293 comment · 13 complexity · b3957cba7cf60684759bcf0079524be6 MD5 · raw file

  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2011, Oracle Corporation, Kohsuke Kawaguchi, Nikita Levyankov
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. *
  13. * The above copyright notice and this permission notice shall be included in
  14. * all copies or substantial portions of the Software.
  15. *
  16. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. * THE SOFTWARE.
  23. */
  24. package hudson.security;
  25. import com.octo.captcha.service.CaptchaServiceException;
  26. import com.octo.captcha.service.image.DefaultManageableImageCaptchaService;
  27. import groovy.lang.Binding;
  28. import hudson.DescriptorExtensionList;
  29. import hudson.EnvVars;
  30. import hudson.Extension;
  31. import hudson.ExtensionPoint;
  32. import hudson.cli.CLICommand;
  33. import hudson.model.AbstractDescribableImpl;
  34. import hudson.model.Descriptor;
  35. import hudson.model.Hudson;
  36. import hudson.security.FederatedLoginService.FederatedIdentity;
  37. import hudson.util.DescriptorList;
  38. import hudson.util.PluginServletFilter;
  39. import hudson.util.spring.BeanBuilder;
  40. import java.io.IOException;
  41. import java.util.Map;
  42. import java.util.logging.Level;
  43. import java.util.logging.Logger;
  44. import javax.imageio.ImageIO;
  45. import javax.servlet.Filter;
  46. import javax.servlet.FilterConfig;
  47. import javax.servlet.ServletException;
  48. import javax.servlet.http.Cookie;
  49. import javax.servlet.http.HttpSession;
  50. import org.acegisecurity.Authentication;
  51. import org.acegisecurity.AuthenticationManager;
  52. import org.acegisecurity.GrantedAuthority;
  53. import org.acegisecurity.GrantedAuthorityImpl;
  54. import org.acegisecurity.context.SecurityContext;
  55. import org.acegisecurity.context.SecurityContextHolder;
  56. import org.acegisecurity.ui.rememberme.RememberMeServices;
  57. import org.acegisecurity.userdetails.UserDetails;
  58. import org.acegisecurity.userdetails.UserDetailsService;
  59. import org.acegisecurity.userdetails.UsernameNotFoundException;
  60. import org.kohsuke.stapler.HttpResponse;
  61. import org.kohsuke.stapler.Stapler;
  62. import org.kohsuke.stapler.StaplerRequest;
  63. import org.kohsuke.stapler.StaplerResponse;
  64. import org.springframework.context.ApplicationContext;
  65. import org.springframework.dao.DataAccessException;
  66. import org.springframework.web.context.WebApplicationContext;
  67. import static org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY;
  68. /**
  69. * Pluggable security realm that connects external user database to Hudson.
  70. * <p/>
  71. * <p/>
  72. * If additional views/URLs need to be exposed,
  73. * an active {@link SecurityRealm} is bound to <tt>CONTEXT_ROOT/securityRealm/</tt>
  74. * through {@link Hudson#getSecurityRealm()}, so you can define additional pages and
  75. * operations on your {@link SecurityRealm}.
  76. * <p/>
  77. * <h2>How do I implement this class?</h2>
  78. * <p/>
  79. * For compatibility reasons, there are two somewhat different ways to implement a custom SecurityRealm.
  80. * <p/>
  81. * <p/>
  82. * One is to override the {@link #createSecurityComponents()} and create key Acegi components
  83. * that control the authentication process.
  84. * The default {@link SecurityRealm#createFilter(FilterConfig)} implementation then assembles them
  85. * into a chain of {@link Filter}s. All the incoming requests to Hudson go through this filter chain,
  86. * and when the filter chain is done, {@link SecurityContext#getAuthentication()} would tell us
  87. * who the current user is.
  88. * <p/>
  89. * <p/>
  90. * If your {@link SecurityRealm} needs to touch the default {@link Filter} chain configuration
  91. * (e.g., adding new ones), then you can also override {@link #createFilter(FilterConfig)} to do so.
  92. * <p/>
  93. * <p/>
  94. * This model is expected to fit most {@link SecurityRealm} implementations.
  95. * <p/>
  96. * <p/>
  97. * <p/>
  98. * The other way of doing this is to ignore {@link #createSecurityComponents()} completely (by returning
  99. * {@link SecurityComponents} created by the default constructor) and just concentrate on {@link #createFilter(FilterConfig)}.
  100. * As long as the resulting filter chain properly sets up {@link Authentication} object at the end of the processing,
  101. * Hudson doesn't really need you to fit the standard Acegi models like {@link AuthenticationManager} and
  102. * {@link UserDetailsService}.
  103. * <p/>
  104. * <p/>
  105. * This model is for those "weird" implementations.
  106. * <p/>
  107. * <p/>
  108. * <h2>Views</h2>
  109. * <dl>
  110. * <dt>loginLink.jelly</dt>
  111. * <dd>
  112. * This view renders the login link on the top right corner of every page, when the user
  113. * is anonymous. For {@link SecurityRealm}s that support user sign-up, this is a good place
  114. * to show a "sign up" link. See {@link HudsonPrivateSecurityRealm} implementation
  115. * for an example of this.
  116. * <p/>
  117. * <dt>config.jelly</dt>
  118. * <dd>
  119. * This view is used to render the configuration page in the system config screen.
  120. * </dl>
  121. *
  122. * @author Kohsuke Kawaguchi
  123. * @author Nikita Levyankov
  124. * @see PluginServletFilter
  125. * @since 1.160
  126. */
  127. public abstract class SecurityRealm extends AbstractDescribableImpl<SecurityRealm> implements ExtensionPoint {
  128. /**
  129. * Creates fully-configured {@link AuthenticationManager} that performs authentication
  130. * against the user realm. The implementation hides how such authentication manager
  131. * is configured.
  132. * <p/>
  133. * <p/>
  134. * {@link AuthenticationManager} instantiation often depends on the user-specified parameters
  135. * (for example, if the authentication is based on LDAP, the user needs to specify
  136. * the host name of the LDAP server.) Such configuration is expected to be
  137. * presented to the user via <tt>config.jelly</tt> and then
  138. * captured as instance variables inside the {@link SecurityRealm} implementation.
  139. * <p/>
  140. * <p/>
  141. * Your {@link SecurityRealm} may also wants to alter {@link Filter} set up by
  142. * overriding {@link #createFilter(FilterConfig)}.
  143. */
  144. public abstract SecurityComponents createSecurityComponents();
  145. /**
  146. * Creates a {@link CliAuthenticator} object that authenticates an invocation of a CLI command.
  147. * See {@link CliAuthenticator} for more details.
  148. *
  149. * @param command The command about to be executed.
  150. * @return never null. By default, this method returns a no-op authenticator that always authenticates
  151. * the session as authenticated by the transport (which is often just {@link Hudson#ANONYMOUS}.)
  152. */
  153. public CliAuthenticator createCliAuthenticator(final CLICommand command) {
  154. return new CliAuthenticator() {
  155. public Authentication authenticate() {
  156. return command.getTransportAuthentication();
  157. }
  158. };
  159. }
  160. /**
  161. * {@inheritDoc}
  162. * <p/>
  163. * <p/>
  164. * {@link SecurityRealm} is a singleton resource in Hudson, and therefore
  165. * it's always configured through <tt>config.jelly</tt> and never with
  166. * <tt>global.jelly</tt>.
  167. */
  168. public Descriptor<SecurityRealm> getDescriptor() {
  169. return super.getDescriptor();
  170. }
  171. /**
  172. * Returns the URL to submit a form for the authentication.
  173. * There's no need to override this, except for {@link LegacySecurityRealm}.
  174. */
  175. public String getAuthenticationGatewayUrl() {
  176. return "j_acegi_security_check";
  177. }
  178. /**
  179. * Gets the target URL of the "login" link.
  180. * There's no need to override this, except for {@link LegacySecurityRealm}.
  181. * On legacy implementation this should point to {@code loginEntry}, which
  182. * is protected by <tt>web.xml</tt>, so that the user can be eventually authenticated
  183. * by the container.
  184. * <p/>
  185. * <p/>
  186. * Path is relative from the context root of the Hudson application.
  187. * The URL returned by this method will get the "from" query parameter indicating
  188. * the page that the user was at.
  189. */
  190. public String getLoginUrl() {
  191. return "login";
  192. }
  193. /**
  194. * Returns true if this {@link SecurityRealm} supports explicit logout operation.
  195. * <p/>
  196. * <p/>
  197. * If the method returns false, "logout" link will not be displayed. This is useful
  198. * when authentication doesn't require an explicit login activity (such as NTLM authentication
  199. * or Kerberos authentication, where Hudson has no ability to log off the current user.)
  200. * <p/>
  201. * <p/>
  202. * By default, this method returns true.
  203. *
  204. * @since 1.307
  205. */
  206. public boolean canLogOut() {
  207. return true;
  208. }
  209. /**
  210. * Controls where the user is sent to after a logout. By default, it's the top page
  211. * of Hudson, but you can return arbitrary URL.
  212. *
  213. * @param req {@link StaplerRequest} that represents the current request. Primarily so that
  214. * you can get the context path. By the time this method is called, the session
  215. * is already invalidated. Never null.
  216. * @param auth The {@link Authentication} object that represents the user that was logging in.
  217. * This parameter allows you to redirect people to different pages depending on who they are.
  218. * @return never null.
  219. * @see #doLogout(StaplerRequest, StaplerResponse)
  220. * @since 1.314
  221. */
  222. protected String getPostLogOutUrl(StaplerRequest req, Authentication auth) {
  223. return req.getContextPath() + "/";
  224. }
  225. /**
  226. * Handles the logout processing.
  227. * <p/>
  228. * <p/>
  229. * The default implementation erases the session and do a few other clean up, then
  230. * redirect the user to the URL specified by {@link #getPostLogOutUrl(StaplerRequest, Authentication)}.
  231. *
  232. * @since 1.314
  233. */
  234. public void doLogout(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
  235. HttpSession session = req.getSession(false);
  236. if (session != null) {
  237. session.invalidate();
  238. }
  239. Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  240. SecurityContextHolder.clearContext();
  241. //Clear env property.
  242. EnvVars.clearHudsonUserEnvVar();
  243. // reset remember-me cookie
  244. Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY, "");
  245. cookie.setPath(req.getContextPath().length() > 0 ? req.getContextPath() : "/");
  246. rsp.addCookie(cookie);
  247. rsp.sendRedirect2(getPostLogOutUrl(req, auth));
  248. }
  249. /**
  250. * Returns true if this {@link SecurityRealm} allows online sign-up.
  251. * This creates a hyperlink that redirects users to <tt>CONTEXT_ROOT/signUp</tt>,
  252. * which will be served by the <tt>signup.jelly</tt> view of this class.
  253. * <p/>
  254. * <p/>
  255. * If the implementation needs to redirect the user to a different URL
  256. * for signing up, use the following jelly script as <tt>signup.jelly</tt>
  257. * <p/>
  258. * <pre><xmp>
  259. * <st:redirect url="http://www.sun.com/" xmlns:st="jelly:stapler"/>
  260. * </xmp></pre>
  261. */
  262. public boolean allowsSignup() {
  263. Class clz = getClass();
  264. return clz.getClassLoader().getResource(clz.getName().replace('.', '/') + "/signup.jelly") != null;
  265. }
  266. /**
  267. * Shortcut for {@link UserDetailsService#loadUserByUsername(String)}.
  268. *
  269. * @return never null.
  270. * @throws UserMayOrMayNotExistException If the security realm cannot even tell if the user exists or not.
  271. */
  272. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
  273. return getSecurityComponents().userDetails.loadUserByUsername(username);
  274. }
  275. /**
  276. * If this {@link SecurityRealm} supports a look up of {@link GroupDetails} by their names, override this method
  277. * to provide the look up.
  278. * <p/>
  279. * <p/>
  280. * This information, when available, can be used by {@link AuthorizationStrategy}s to improve the UI and
  281. * error diagnostics for the user.
  282. */
  283. public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
  284. throw new UserMayOrMayNotExistException(groupname);
  285. }
  286. /**
  287. * Starts the user registration process for a new user that has the given verified identity.
  288. * <p/>
  289. * <p/>
  290. * If the user logs in through a {@link FederatedLoginService}, verified that the current user
  291. * owns an {@linkplain FederatedIdentity identity}, but no existing user account has claimed that identity,
  292. * then this method is invoked.
  293. * <p/>
  294. * <p/>
  295. * The expected behaviour is to confirm that the user would like to create a new account, and
  296. * associate this federated identity to the newly created account (via {@link FederatedIdentity#addToCurrentUser()}.
  297. *
  298. * @throws UnsupportedOperationException If this implementation doesn't support the signup through this mechanism.
  299. * This is the default implementation.
  300. * @since 1.394
  301. */
  302. public HttpResponse commenceSignup(FederatedIdentity identity) {
  303. throw new UnsupportedOperationException();
  304. }
  305. /**
  306. * {@link DefaultManageableImageCaptchaService} holder to defer initialization.
  307. */
  308. private static final class CaptchaService {
  309. private static final DefaultManageableImageCaptchaService INSTANCE = new DefaultManageableImageCaptchaService();
  310. }
  311. /**
  312. * Generates a captcha image.
  313. */
  314. public final void doCaptcha(StaplerRequest req, StaplerResponse rsp) throws IOException {
  315. String id = req.getSession().getId();
  316. rsp.setContentType("image/png");
  317. rsp.addHeader("Cache-Control", "no-cache");
  318. ImageIO.write(CaptchaService.INSTANCE.getImageChallengeForID(id), "PNG", rsp.getOutputStream());
  319. }
  320. /**
  321. * Validates the captcha.
  322. */
  323. protected final boolean validateCaptcha(String text) {
  324. try {
  325. String id = Stapler.getCurrentRequest().getSession().getId();
  326. Boolean b = CaptchaService.INSTANCE.validateResponseForID(id, text);
  327. return b != null && b;
  328. } catch (CaptchaServiceException e) {
  329. LOGGER.log(Level.INFO, "Captcha validation had a problem", e);
  330. return false;
  331. }
  332. }
  333. /**
  334. * Picks up the instance of the given type from the spring context.
  335. * If there are multiple beans of the same type or if there are none,
  336. * this method treats that as an {@link IllegalArgumentException}.
  337. * <p/>
  338. * This method is intended to be used to pick up a Acegi object from
  339. * spring once the bean definition file is parsed.
  340. */
  341. protected static <T> T findBean(Class<T> type, ApplicationContext context) {
  342. Map m = context.getBeansOfType(type);
  343. switch (m.size()) {
  344. case 0:
  345. throw new IllegalArgumentException("No beans of " + type + " are defined");
  346. case 1:
  347. return type.cast(m.values().iterator().next());
  348. default:
  349. throw new IllegalArgumentException("Multiple beans of " + type + " are defined: " + m);
  350. }
  351. }
  352. /**
  353. * Holder for the SecurityComponents.
  354. */
  355. private transient SecurityComponents securityComponents;
  356. /**
  357. * Use this function to get the security components, without necessarily
  358. * recreating them.
  359. */
  360. public synchronized SecurityComponents getSecurityComponents() {
  361. if (this.securityComponents == null) {
  362. this.securityComponents = this.createSecurityComponents();
  363. }
  364. return this.securityComponents;
  365. }
  366. /**
  367. * Creates {@link Filter} that all the incoming HTTP requests will go through
  368. * for authentication.
  369. * <p/>
  370. * <p/>
  371. * The default implementation uses {@link #getSecurityComponents()} and builds
  372. * a standard filter chain from /WEB-INF/security/SecurityFilters.groovy.
  373. * But subclasses can override this to completely change the filter sequence.
  374. * <p/>
  375. * <p/>
  376. * For other plugins that want to contribute {@link Filter}, see
  377. * {@link PluginServletFilter}.
  378. *
  379. * @since 1.271
  380. */
  381. public Filter createFilter(FilterConfig filterConfig) {
  382. LOGGER.entering(SecurityRealm.class.getName(), "createFilter");
  383. Binding binding = new Binding();
  384. SecurityComponents sc = getSecurityComponents();
  385. binding.setVariable("securityComponents", sc);
  386. binding.setVariable("securityRealm", this);
  387. BeanBuilder builder = new BeanBuilder();
  388. builder.parse(filterConfig.getServletContext().getResourceAsStream("/WEB-INF/security/SecurityFilters.groovy"),
  389. binding);
  390. WebApplicationContext context = builder.createApplicationContext();
  391. return (Filter) context.getBean("filter");
  392. }
  393. /**
  394. * Singleton constant that represents "no authentication."
  395. */
  396. public static final SecurityRealm NO_AUTHENTICATION = new None();
  397. private static class None extends SecurityRealm {
  398. public SecurityComponents createSecurityComponents() {
  399. return new SecurityComponents(new AuthenticationManager() {
  400. public Authentication authenticate(Authentication authentication) {
  401. return authentication;
  402. }
  403. }, new UserDetailsService() {
  404. public UserDetails loadUserByUsername(String username)
  405. throws UsernameNotFoundException, DataAccessException {
  406. throw new UsernameNotFoundException(username);
  407. }
  408. });
  409. }
  410. /**
  411. * This special instance is not configurable explicitly,
  412. * so it doesn't have a descriptor.
  413. */
  414. @Override
  415. public Descriptor<SecurityRealm> getDescriptor() {
  416. return null;
  417. }
  418. /**
  419. * There's no group.
  420. */
  421. @Override
  422. public GroupDetails loadGroupByGroupname(String groupname)
  423. throws UsernameNotFoundException, DataAccessException {
  424. throw new UsernameNotFoundException(groupname);
  425. }
  426. /**
  427. * We don't need any filter for this {@link SecurityRealm}.
  428. */
  429. @Override
  430. public Filter createFilter(FilterConfig filterConfig) {
  431. return new ChainedServletFilter();
  432. }
  433. /**
  434. * Maintain singleton semantics.
  435. */
  436. private Object readResolve() {
  437. return NO_AUTHENTICATION;
  438. }
  439. }
  440. /**
  441. * Just a tuple so that we can create various inter-related security related objects and
  442. * return them all at once.
  443. * <p/>
  444. * <p/>
  445. * None of the fields are ever null.
  446. *
  447. * @see SecurityRealm#createSecurityComponents()
  448. */
  449. public static final class SecurityComponents {
  450. //TODO: review and check whether we can do it private
  451. public final AuthenticationManager manager;
  452. public final UserDetailsService userDetails;
  453. public final RememberMeServices rememberMe;
  454. public SecurityComponents() {
  455. // we use AuthenticationManagerProxy here just as an implementation that fails all the time,
  456. // not as a proxy. No one is supposed to use this as a proxy.
  457. this(new AuthenticationManagerProxy());
  458. }
  459. public SecurityComponents(AuthenticationManager manager) {
  460. // we use UserDetailsServiceProxy here just as an implementation that fails all the time,
  461. // not as a proxy. No one is supposed to use this as a proxy.
  462. this(manager, new UserDetailsServiceProxy());
  463. }
  464. public SecurityComponents(AuthenticationManager manager, UserDetailsService userDetails) {
  465. this(manager, userDetails, createRememberMeService(userDetails));
  466. }
  467. public SecurityComponents(AuthenticationManager manager, UserDetailsService userDetails,
  468. RememberMeServices rememberMe) {
  469. assert manager != null && userDetails != null && rememberMe != null;
  470. this.manager = manager;
  471. this.userDetails = userDetails;
  472. this.rememberMe = rememberMe;
  473. }
  474. public AuthenticationManager getManager() {
  475. return manager;
  476. }
  477. public UserDetailsService getUserDetails() {
  478. return userDetails;
  479. }
  480. public RememberMeServices getRememberMe() {
  481. return rememberMe;
  482. }
  483. private static RememberMeServices createRememberMeService(UserDetailsService uds) {
  484. // create our default TokenBasedRememberMeServices, which depends on the availability of the secret key
  485. TokenBasedRememberMeServices2 rms = new TokenBasedRememberMeServices2();
  486. rms.setUserDetailsService(uds);
  487. rms.setKey(Hudson.getInstance().getSecretKey());
  488. rms.setParameter("remember_me"); // this is the form field name in login.jelly
  489. return rms;
  490. }
  491. }
  492. /**
  493. * All registered {@link SecurityRealm} implementations.
  494. *
  495. * @deprecated as of 1.286
  496. * Use {@link #all()} for read access, and use {@link Extension} for registration.
  497. */
  498. public static final DescriptorList<SecurityRealm> LIST = new DescriptorList<SecurityRealm>(SecurityRealm.class);
  499. /**
  500. * Returns all the registered {@link SecurityRealm} descriptors.
  501. */
  502. public static DescriptorExtensionList<SecurityRealm, Descriptor<SecurityRealm>> all() {
  503. return Hudson.getInstance().<SecurityRealm, Descriptor<SecurityRealm>>getDescriptorList(SecurityRealm.class);
  504. }
  505. private static final Logger LOGGER = Logger.getLogger(SecurityRealm.class.getName());
  506. /**
  507. * {@link GrantedAuthority} that represents the built-in "authenticated" role, which is granted to
  508. * anyone non-anonymous.
  509. */
  510. public static final GrantedAuthority AUTHENTICATED_AUTHORITY = new GrantedAuthorityImpl("authenticated");
  511. }