--- /dev/null
+//
+// ========================================================================
+// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.security;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSessionEvent;
+import javax.servlet.http.HttpSessionListener;
+
+import org.eclipse.jetty.security.authentication.DeferredAuthentication;
+import org.eclipse.jetty.server.Authentication;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.ContextHandler.Context;
+import org.eclipse.jetty.server.handler.HandlerWrapper;
+import org.eclipse.jetty.server.session.AbstractSession;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/**
+ * Abstract SecurityHandler.
+ * Select and apply an {@link Authenticator} to a request.
+ * <p>
+ * The Authenticator may either be directly set on the handler
+ * or will be create during {@link #start()} with a call to
+ * either the default or set AuthenticatorFactory.
+ * <p>
+ * SecurityHandler has a set of initparameters that are used by the
+ * Authentication.Configuration. At startup, any context init parameters
+ * that start with "org.eclipse.jetty.security." that do not have
+ * values in the SecurityHandler init parameters, are copied.
+ *
+ */
+public abstract class SecurityHandler extends HandlerWrapper implements Authenticator.AuthConfiguration
+{
+ private static final Logger LOG = Log.getLogger(SecurityHandler.class);
+
+ /* ------------------------------------------------------------ */
+ private boolean _checkWelcomeFiles = false;
+ private Authenticator _authenticator;
+ private Authenticator.Factory _authenticatorFactory=new DefaultAuthenticatorFactory();
+ private String _realmName;
+ private String _authMethod;
+ private final Map<String,String> _initParameters=new HashMap<String,String>();
+ private LoginService _loginService;
+ private IdentityService _identityService;
+ private boolean _renewSession=true;
+
+ /* ------------------------------------------------------------ */
+ protected SecurityHandler()
+ {
+ addBean(_authenticatorFactory);
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Get the identityService.
+ * @return the identityService
+ */
+ @Override
+ public IdentityService getIdentityService()
+ {
+ return _identityService;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Set the identityService.
+ * @param identityService the identityService to set
+ */
+ public void setIdentityService(IdentityService identityService)
+ {
+ if (isStarted())
+ throw new IllegalStateException("Started");
+ updateBean(_identityService,identityService);
+ _identityService = identityService;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Get the loginService.
+ * @return the loginService
+ */
+ @Override
+ public LoginService getLoginService()
+ {
+ return _loginService;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Set the loginService.
+ * @param loginService the loginService to set
+ */
+ public void setLoginService(LoginService loginService)
+ {
+ if (isStarted())
+ throw new IllegalStateException("Started");
+ updateBean(_loginService,loginService);
+ _loginService = loginService;
+ }
+
+
+ /* ------------------------------------------------------------ */
+ public Authenticator getAuthenticator()
+ {
+ return _authenticator;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Set the authenticator.
+ * @param authenticator
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setAuthenticator(Authenticator authenticator)
+ {
+ if (isStarted())
+ throw new IllegalStateException("Started");
+ updateBean(_authenticator,authenticator);
+ _authenticator = authenticator;
+ if (_authenticator!=null)
+ _authMethod=_authenticator.getAuthMethod();
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @return the authenticatorFactory
+ */
+ public Authenticator.Factory getAuthenticatorFactory()
+ {
+ return _authenticatorFactory;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param authenticatorFactory the authenticatorFactory to set
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setAuthenticatorFactory(Authenticator.Factory authenticatorFactory)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ updateBean(_authenticatorFactory,authenticatorFactory);
+ _authenticatorFactory = authenticatorFactory;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @return the realmName
+ */
+ @Override
+ public String getRealmName()
+ {
+ return _realmName;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param realmName the realmName to set
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setRealmName(String realmName)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ _realmName = realmName;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @return the authMethod
+ */
+ @Override
+ public String getAuthMethod()
+ {
+ return _authMethod;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param authMethod the authMethod to set
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setAuthMethod(String authMethod)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ _authMethod = authMethod;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @return True if forwards to welcome files are authenticated
+ */
+ public boolean isCheckWelcomeFiles()
+ {
+ return _checkWelcomeFiles;
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param authenticateWelcomeFiles True if forwards to welcome files are
+ * authenticated
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public void setCheckWelcomeFiles(boolean authenticateWelcomeFiles)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ _checkWelcomeFiles = authenticateWelcomeFiles;
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public String getInitParameter(String key)
+ {
+ return _initParameters.get(key);
+ }
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public Set<String> getInitParameterNames()
+ {
+ return _initParameters.keySet();
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Set an initialization parameter.
+ * @param key
+ * @param value
+ * @return previous value
+ * @throws IllegalStateException if the SecurityHandler is running
+ */
+ public String setInitParameter(String key, String value)
+ {
+ if (isRunning())
+ throw new IllegalStateException("running");
+ return _initParameters.put(key,value);
+ }
+
+ /* ------------------------------------------------------------ */
+ protected LoginService findLoginService() throws Exception
+ {
+ Collection<LoginService> list = getServer().getBeans(LoginService.class);
+ LoginService service = null;
+ String realm=getRealmName();
+ if (realm!=null)
+ {
+ for (LoginService s : list)
+ if (s.getName()!=null && s.getName().equals(realm))
+ {
+ service=s;
+ break;
+ }
+ }
+ else if (list.size()==1)
+ service = list.iterator().next();
+
+ return service;
+ }
+
+ /* ------------------------------------------------------------ */
+ protected IdentityService findIdentityService()
+ {
+ return getServer().getBean(IdentityService.class);
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ */
+ @Override
+ protected void doStart()
+ throws Exception
+ {
+ // copy security init parameters
+ ContextHandler.Context context =ContextHandler.getCurrentContext();
+ if (context!=null)
+ {
+ Enumeration<String> names=context.getInitParameterNames();
+ while (names!=null && names.hasMoreElements())
+ {
+ String name =names.nextElement();
+ if (name.startsWith("org.eclipse.jetty.security.") &&
+ getInitParameter(name)==null)
+ setInitParameter(name,context.getInitParameter(name));
+ }
+
+ //register a session listener to handle securing sessions when authentication is performed
+ context.getContextHandler().addEventListener(new HttpSessionListener()
+ {
+ @Override
+ public void sessionDestroyed(HttpSessionEvent se)
+ {
+ }
+
+ @Override
+ public void sessionCreated(HttpSessionEvent se)
+ {
+ //if current request is authenticated, then as we have just created the session, mark it as secure, as it has not yet been returned to a user
+ HttpChannel<?> channel = HttpChannel.getCurrentHttpChannel();
+
+ if (channel == null)
+ return;
+ Request request = channel.getRequest();
+ if (request == null)
+ return;
+
+ if (request.isSecure())
+ {
+ se.getSession().setAttribute(AbstractSession.SESSION_KNOWN_ONLY_TO_AUTHENTICATED, Boolean.TRUE);
+ }
+ }
+ });
+ }
+
+ // complicated resolution of login and identity service to handle
+ // many different ways these can be constructed and injected.
+
+ if (_loginService==null)
+ {
+ setLoginService(findLoginService());
+ if (_loginService!=null)
+ unmanage(_loginService);
+ }
+
+ if (_identityService==null)
+ {
+ if (_loginService!=null)
+ setIdentityService(_loginService.getIdentityService());
+
+ if (_identityService==null)
+ setIdentityService(findIdentityService());
+
+ if (_identityService==null)
+ {
+ if (_realmName!=null)
+ {
+ setIdentityService(new DefaultIdentityService());
+ manage(_identityService);
+ }
+ }
+ else
+ unmanage(_identityService);
+ }
+
+ if (_loginService!=null)
+ {
+ if (_loginService.getIdentityService()==null)
+ _loginService.setIdentityService(_identityService);
+ else if (_loginService.getIdentityService()!=_identityService)
+ throw new IllegalStateException("LoginService has different IdentityService to "+this);
+ }
+
+ Authenticator.Factory authenticatorFactory = getAuthenticatorFactory();
+ if (_authenticator==null && authenticatorFactory!=null && _identityService!=null)
+ setAuthenticator(authenticatorFactory.getAuthenticator(getServer(),ContextHandler.getCurrentContext(),this, _identityService, _loginService));
+
+ if (_authenticator!=null)
+ _authenticator.setConfiguration(this);
+ else if (_realmName!=null)
+ {
+ LOG.warn("No Authenticator for "+this);
+ throw new IllegalStateException("No Authenticator");
+ }
+
+ super.doStart();
+ }
+
+ @Override
+ /* ------------------------------------------------------------ */
+ protected void doStop() throws Exception
+ {
+ //if we discovered the services (rather than had them explicitly configured), remove them.
+ if (!isManaged(_identityService))
+ {
+ removeBean(_identityService);
+ _identityService = null;
+ }
+
+ if (!isManaged(_loginService))
+ {
+ removeBean(_loginService);
+ _loginService=null;
+ }
+
+ super.doStop();
+ }
+
+ /* ------------------------------------------------------------ */
+ protected boolean checkSecurity(Request request)
+ {
+ switch(request.getDispatcherType())
+ {
+ case REQUEST:
+ case ASYNC:
+ return true;
+ case FORWARD:
+ if (isCheckWelcomeFiles() && request.getAttribute("org.eclipse.jetty.server.welcome") != null)
+ {
+ request.removeAttribute("org.eclipse.jetty.server.welcome");
+ return true;
+ }
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @see org.eclipse.jetty.security.Authenticator.AuthConfiguration#isSessionRenewedOnAuthentication()
+ */
+ @Override
+ public boolean isSessionRenewedOnAuthentication()
+ {
+ return _renewSession;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Set renew the session on Authentication.
+ * <p>
+ * If set to true, then on authentication, the session associated with a reqeuest is invalidated and replaced with a new session.
+ * @see org.eclipse.jetty.security.Authenticator.AuthConfiguration#isSessionRenewedOnAuthentication()
+ */
+ public void setSessionRenewedOnAuthentication(boolean renew)
+ {
+ _renewSession=renew;
+ }
+
+ /* ------------------------------------------------------------ */
+ /*
+ * @see org.eclipse.jetty.server.Handler#handle(java.lang.String,
+ * javax.servlet.http.HttpServletRequest,
+ * javax.servlet.http.HttpServletResponse, int)
+ */
+ @Override
+ public void handle(String pathInContext, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
+ {
+ final Response base_response = baseRequest.getResponse();
+ final Handler handler=getHandler();
+
+ if (handler==null)
+ return;
+
+ final Authenticator authenticator = _authenticator;
+
+ if (checkSecurity(baseRequest))
+ {
+ //See Servlet Spec 3.1 sec 13.6.3
+ if (authenticator != null)
+ authenticator.prepareRequest(baseRequest);
+
+ RoleInfo roleInfo = prepareConstraintInfo(pathInContext, baseRequest);
+
+ // Check data constraints
+ if (!checkUserDataPermissions(pathInContext, baseRequest, base_response, roleInfo))
+ {
+ if (!baseRequest.isHandled())
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ baseRequest.setHandled(true);
+ }
+ return;
+ }
+
+ // is Auth mandatory?
+ boolean isAuthMandatory =
+ isAuthMandatory(baseRequest, base_response, roleInfo);
+
+ if (isAuthMandatory && authenticator==null)
+ {
+ LOG.warn("No authenticator for: "+roleInfo);
+ if (!baseRequest.isHandled())
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ baseRequest.setHandled(true);
+ }
+ return;
+ }
+
+ // check authentication
+ Object previousIdentity = null;
+ try
+ {
+ Authentication authentication = baseRequest.getAuthentication();
+ if (authentication==null || authentication==Authentication.NOT_CHECKED)
+ authentication=authenticator==null?Authentication.UNAUTHENTICATED:authenticator.validateRequest(request, response, isAuthMandatory);
+
+ if (authentication instanceof Authentication.Wrapped)
+ {
+ request=((Authentication.Wrapped)authentication).getHttpServletRequest();
+ response=((Authentication.Wrapped)authentication).getHttpServletResponse();
+ }
+
+ if (authentication instanceof Authentication.ResponseSent)
+ {
+ baseRequest.setHandled(true);
+ }
+ else if (authentication instanceof Authentication.User)
+ {
+ Authentication.User userAuth = (Authentication.User)authentication;
+ baseRequest.setAuthentication(authentication);
+ if (_identityService!=null)
+ previousIdentity = _identityService.associate(userAuth.getUserIdentity());
+
+ if (isAuthMandatory)
+ {
+ boolean authorized=checkWebResourcePermissions(pathInContext, baseRequest, base_response, roleInfo, userAuth.getUserIdentity());
+ if (!authorized)
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "!role");
+ baseRequest.setHandled(true);
+ return;
+ }
+ }
+
+ handler.handle(pathInContext, baseRequest, request, response);
+ if (authenticator!=null)
+ authenticator.secureResponse(request, response, isAuthMandatory, userAuth);
+ }
+ else if (authentication instanceof Authentication.Deferred)
+ {
+ DeferredAuthentication deferred= (DeferredAuthentication)authentication;
+ baseRequest.setAuthentication(authentication);
+
+ try
+ {
+ handler.handle(pathInContext, baseRequest, request, response);
+ }
+ finally
+ {
+ previousIdentity = deferred.getPreviousAssociation();
+ }
+
+ if (authenticator!=null)
+ {
+ Authentication auth=baseRequest.getAuthentication();
+ if (auth instanceof Authentication.User)
+ {
+ Authentication.User userAuth = (Authentication.User)auth;
+ authenticator.secureResponse(request, response, isAuthMandatory, userAuth);
+ }
+ else
+ authenticator.secureResponse(request, response, isAuthMandatory, null);
+ }
+ }
+ else
+ {
+ baseRequest.setAuthentication(authentication);
+ if (_identityService!=null)
+ previousIdentity = _identityService.associate(null);
+ handler.handle(pathInContext, baseRequest, request, response);
+ if (authenticator!=null)
+ authenticator.secureResponse(request, response, isAuthMandatory, null);
+ }
+ }
+ catch (ServerAuthException e)
+ {
+ // jaspi 3.8.3 send HTTP 500 internal server error, with message
+ // from AuthException
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ finally
+ {
+ if (_identityService!=null)
+ _identityService.disassociate(previousIdentity);
+ }
+ }
+ else
+ handler.handle(pathInContext, baseRequest, request, response);
+ }
+
+
+ /* ------------------------------------------------------------ */
+ public static SecurityHandler getCurrentSecurityHandler()
+ {
+ Context context = ContextHandler.getCurrentContext();
+ if (context==null)
+ return null;
+
+ return context.getContextHandler().getChildHandlerByClass(SecurityHandler.class);
+ }
+
+ /* ------------------------------------------------------------ */
+ public void logout(Authentication.User user)
+ {
+ LOG.debug("logout {}",user);
+ LoginService login_service=getLoginService();
+ if (login_service!=null)
+ {
+ login_service.logout(user.getUserIdentity());
+ }
+
+ IdentityService identity_service=getIdentityService();
+ if (identity_service!=null)
+ {
+ // TODO recover previous from threadlocal (or similar)
+ Object previous=null;
+ identity_service.disassociate(previous);
+ }
+ }
+
+ /* ------------------------------------------------------------ */
+ protected abstract RoleInfo prepareConstraintInfo(String pathInContext, Request request);
+
+ /* ------------------------------------------------------------ */
+ protected abstract boolean checkUserDataPermissions(String pathInContext, Request request, Response response, RoleInfo constraintInfo) throws IOException;
+
+ /* ------------------------------------------------------------ */
+ protected abstract boolean isAuthMandatory(Request baseRequest, Response base_response, Object constraintInfo);
+
+ /* ------------------------------------------------------------ */
+ protected abstract boolean checkWebResourcePermissions(String pathInContext, Request request, Response response, Object constraintInfo,
+ UserIdentity userIdentity) throws IOException;
+
+
+ /* ------------------------------------------------------------ */
+ /* ------------------------------------------------------------ */
+ public class NotChecked implements Principal
+ {
+ @Override
+ public String getName()
+ {
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "NOT CHECKED";
+ }
+
+ public SecurityHandler getSecurityHandler()
+ {
+ return SecurityHandler.this;
+ }
+ }
+
+
+ /* ------------------------------------------------------------ */
+ /* ------------------------------------------------------------ */
+ public static final Principal __NO_USER = new Principal()
+ {
+ @Override
+ public String getName()
+ {
+ return null;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "No User";
+ }
+ };
+
+ /* ------------------------------------------------------------ */
+ /* ------------------------------------------------------------ */
+ /**
+ * Nobody user. The Nobody UserPrincipal is used to indicate a partial state
+ * of authentication. A request with a Nobody UserPrincipal will be allowed
+ * past all authentication constraints - but will not be considered an
+ * authenticated request. It can be used by Authenticators such as
+ * FormAuthenticator to allow access to logon and error pages within an
+ * authenticated URI tree.
+ */
+ public static final Principal __NOBODY = new Principal()
+ {
+ @Override
+ public String getName()
+ {
+ return "Nobody";
+ }
+
+ @Override
+ public String toString()
+ {
+ return getName();
+ }
+ };
+
+}