blob: 267187145c71ea2cdc79396a60939cfd003c5524 [file] [log] [blame]
//
// ========================================================================
// 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import org.eclipse.jetty.http.HttpSchemes;
import javax.servlet.HttpConstraintElement;
import javax.servlet.HttpMethodConstraintElement;
import javax.servlet.ServletSecurityElement;
import javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
import javax.servlet.annotation.ServletSecurity.TransportGuarantee;
import org.eclipse.jetty.http.PathMap;
import org.eclipse.jetty.server.AbstractHttpConnection;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.StringMap;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.security.Constraint;
/* ------------------------------------------------------------ */
/**
* Handler to enforce SecurityConstraints. This implementation is servlet spec
* 3.0 compliant and precomputes the constraint combinations for runtime
* efficiency.
*
*/
public class ConstraintSecurityHandler extends SecurityHandler implements ConstraintAware
{
private static final String OMISSION_SUFFIX = ".omission";
private final List<ConstraintMapping> _constraintMappings= new CopyOnWriteArrayList<ConstraintMapping>();
private final Set<String> _roles = new CopyOnWriteArraySet<String>();
private final PathMap _constraintMap = new PathMap();
private boolean _strict = true;
/* ------------------------------------------------------------ */
/**
* @return
*/
public static Constraint createConstraint()
{
return new Constraint();
}
/* ------------------------------------------------------------ */
/**
* @param constraint
* @return
*/
public static Constraint createConstraint(Constraint constraint)
{
try
{
return (Constraint)constraint.clone();
}
catch (CloneNotSupportedException e)
{
throw new IllegalStateException (e);
}
}
/* ------------------------------------------------------------ */
/**
* Create a security constraint
*
* @param name
* @param authenticate
* @param roles
* @param dataConstraint
* @return
*/
public static Constraint createConstraint (String name, boolean authenticate, String[] roles, int dataConstraint)
{
Constraint constraint = createConstraint();
if (name != null)
constraint.setName(name);
constraint.setAuthenticate(authenticate);
constraint.setRoles(roles);
constraint.setDataConstraint(dataConstraint);
return constraint;
}
/* ------------------------------------------------------------ */
/**
* @param name
* @param element
* @return
*/
public static Constraint createConstraint (String name, HttpConstraintElement element)
{
return createConstraint(name, element.getRolesAllowed(), element.getEmptyRoleSemantic(), element.getTransportGuarantee());
}
/* ------------------------------------------------------------ */
/**
* @param name
* @param rolesAllowed
* @param permitOrDeny
* @param transport
* @return
*/
public static Constraint createConstraint (String name, String[] rolesAllowed, EmptyRoleSemantic permitOrDeny, TransportGuarantee transport)
{
Constraint constraint = createConstraint();
if (rolesAllowed == null || rolesAllowed.length==0)
{
if (permitOrDeny.equals(EmptyRoleSemantic.DENY))
{
//Equivalent to <auth-constraint> with no roles
constraint.setName(name+"-Deny");
constraint.setAuthenticate(true);
}
else
{
//Equivalent to no <auth-constraint>
constraint.setName(name+"-Permit");
constraint.setAuthenticate(false);
}
}
else
{
//Equivalent to <auth-constraint> with list of <security-role-name>s
constraint.setAuthenticate(true);
constraint.setRoles(rolesAllowed);
constraint.setName(name+"-RolesAllowed");
}
//Equivalent to //<user-data-constraint><transport-guarantee>CONFIDENTIAL</transport-guarantee></user-data-constraint>
constraint.setDataConstraint((transport.equals(TransportGuarantee.CONFIDENTIAL)?Constraint.DC_CONFIDENTIAL:Constraint.DC_NONE));
return constraint;
}
/* ------------------------------------------------------------ */
/**
* @param pathSpec
* @param constraintMappings
* @return
*/
public static List<ConstraintMapping> getConstraintMappingsForPath(String pathSpec, List<ConstraintMapping> constraintMappings)
{
if (pathSpec == null || "".equals(pathSpec.trim()) || constraintMappings == null || constraintMappings.size() == 0)
return Collections.emptyList();
List<ConstraintMapping> mappings = new ArrayList<ConstraintMapping>();
for (ConstraintMapping mapping:constraintMappings)
{
if (pathSpec.equals(mapping.getPathSpec()))
{
mappings.add(mapping);
}
}
return mappings;
}
/* ------------------------------------------------------------ */
/** Take out of the constraint mappings those that match the
* given path.
*
* @param pathSpec
* @param constraintMappings a new list minus the matching constraints
* @return
*/
public static List<ConstraintMapping> removeConstraintMappingsForPath(String pathSpec, List<ConstraintMapping> constraintMappings)
{
if (pathSpec == null || "".equals(pathSpec.trim()) || constraintMappings == null || constraintMappings.size() == 0)
return Collections.emptyList();
List<ConstraintMapping> mappings = new ArrayList<ConstraintMapping>();
for (ConstraintMapping mapping:constraintMappings)
{
//Remove the matching mappings by only copying in non-matching mappings
if (!pathSpec.equals(mapping.getPathSpec()))
{
mappings.add(mapping);
}
}
return mappings;
}
/* ------------------------------------------------------------ */
/** Generate Constraints and ContraintMappings for the given url pattern and ServletSecurityElement
*
* @param name
* @param pathSpec
* @param securityElement
* @return
*/
public static List<ConstraintMapping> createConstraintsWithMappingsForPath (String name, String pathSpec, ServletSecurityElement securityElement)
{
List<ConstraintMapping> mappings = new ArrayList<ConstraintMapping>();
//Create a constraint that will describe the default case (ie if not overridden by specific HttpMethodConstraints)
Constraint constraint = ConstraintSecurityHandler.createConstraint(name, securityElement);
//Create a mapping for the pathSpec for the default case
ConstraintMapping defaultMapping = new ConstraintMapping();
defaultMapping.setPathSpec(pathSpec);
defaultMapping.setConstraint(constraint);
mappings.add(defaultMapping);
//See Spec 13.4.1.2 p127
List<String> methodOmissions = new ArrayList<String>();
//make constraint mappings for this url for each of the HttpMethodConstraintElements
Collection<HttpMethodConstraintElement> methodConstraints = securityElement.getHttpMethodConstraints();
if (methodConstraints != null)
{
for (HttpMethodConstraintElement methodConstraint:methodConstraints)
{
//Make a Constraint that captures the <auth-constraint> and <user-data-constraint> elements supplied for the HttpMethodConstraintElement
Constraint mconstraint = ConstraintSecurityHandler.createConstraint(name, methodConstraint);
ConstraintMapping mapping = new ConstraintMapping();
mapping.setConstraint(mconstraint);
mapping.setPathSpec(pathSpec);
if (methodConstraint.getMethodName() != null)
{
mapping.setMethod(methodConstraint.getMethodName());
//See spec 13.4.1.2 p127 - add an omission for every method name to the default constraint
methodOmissions.add(methodConstraint.getMethodName());
}
mappings.add(mapping);
}
}
//See spec 13.4.1.2 p127 - add an omission for every method name to the default constraint
if (methodOmissions.size() > 0)
defaultMapping.setMethodOmissions(methodOmissions.toArray(new String[methodOmissions.size()]));
return mappings;
}
/* ------------------------------------------------------------ */
/** Get the strict mode.
* @return true if the security handler is running in strict mode.
*/
public boolean isStrict()
{
return _strict;
}
/* ------------------------------------------------------------ */
/** Set the strict mode of the security handler.
* <p>
* When in strict mode (the default), the full servlet specification
* will be implemented.
* If not in strict mode, some additional flexibility in configuration
* is allowed:<ul>
* <li>All users do not need to have a role defined in the deployment descriptor
* <li>The * role in a constraint applies to ANY role rather than all roles defined in
* the deployment descriptor.
* </ul>
*
* @param strict the strict to set
* @see #setRoles(Set)
* @see #setConstraintMappings(List, Set)
*/
public void setStrict(boolean strict)
{
_strict = strict;
}
/* ------------------------------------------------------------ */
/**
* @return Returns the constraintMappings.
*/
public List<ConstraintMapping> getConstraintMappings()
{
return _constraintMappings;
}
/* ------------------------------------------------------------ */
public Set<String> getRoles()
{
return _roles;
}
/* ------------------------------------------------------------ */
/**
* Process the constraints following the combining rules in Servlet 3.0 EA
* spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
*
* @param constraintMappings
* The constraintMappings to set, from which the set of known roles
* is determined.
*/
public void setConstraintMappings(List<ConstraintMapping> constraintMappings)
{
setConstraintMappings(constraintMappings,null);
}
/**
* Process the constraints following the combining rules in Servlet 3.0 EA
* spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
*
* @param constraintMappings
* The constraintMappings to set as array, from which the set of known roles
* is determined. Needed to retain API compatibility for 7.x
*/
public void setConstraintMappings( ConstraintMapping[] constraintMappings )
{
setConstraintMappings( Arrays.asList(constraintMappings), null);
}
/* ------------------------------------------------------------ */
/**
* Process the constraints following the combining rules in Servlet 3.0 EA
* spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
*
* @param constraintMappings
* The constraintMappings to set.
* @param roles The known roles (or null to determine them from the mappings)
*/
public void setConstraintMappings(List<ConstraintMapping> constraintMappings, Set<String> roles)
{
_constraintMappings.clear();
_constraintMappings.addAll(constraintMappings);
if (roles==null)
{
roles = new HashSet<String>();
for (ConstraintMapping cm : constraintMappings)
{
String[] cmr = cm.getConstraint().getRoles();
if (cmr!=null)
{
for (String r : cmr)
if (!"*".equals(r))
roles.add(r);
}
}
}
setRoles(roles);
if (isStarted())
{
for (ConstraintMapping mapping : _constraintMappings)
{
processConstraintMapping(mapping);
}
}
}
/* ------------------------------------------------------------ */
/**
* Set the known roles.
* This may be overridden by a subsequent call to {@link #setConstraintMappings(ConstraintMapping[])} or
* {@link #setConstraintMappings(List, Set)}.
* @see #setStrict(boolean)
* @param roles The known roles (or null to determine them from the mappings)
*/
public void setRoles(Set<String> roles)
{
_roles.clear();
_roles.addAll(roles);
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.security.ConstraintAware#addConstraintMapping(org.eclipse.jetty.security.ConstraintMapping)
*/
public void addConstraintMapping(ConstraintMapping mapping)
{
_constraintMappings.add(mapping);
if (mapping.getConstraint()!=null && mapping.getConstraint().getRoles()!=null)
for (String role : mapping.getConstraint().getRoles())
addRole(role);
if (isStarted())
{
processConstraintMapping(mapping);
}
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.security.ConstraintAware#addRole(java.lang.String)
*/
public void addRole(String role)
{
boolean modified = _roles.add(role);
if (isStarted() && modified && _strict)
{
// Add the new role to currently defined any role role infos
for (Map<String,RoleInfo> map : (Collection<Map<String,RoleInfo>>)_constraintMap.values())
{
for (RoleInfo info : map.values())
{
if (info.isAnyRole())
info.addRole(role);
}
}
}
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.security.SecurityHandler#doStart()
*/
@Override
protected void doStart() throws Exception
{
_constraintMap.clear();
if (_constraintMappings!=null)
{
for (ConstraintMapping mapping : _constraintMappings)
{
processConstraintMapping(mapping);
}
}
super.doStart();
}
/* ------------------------------------------------------------ */
@Override
protected void doStop() throws Exception
{
_constraintMap.clear();
_constraintMappings.clear();
_roles.clear();
super.doStop();
}
/* ------------------------------------------------------------ */
/**
* Create and combine the constraint with the existing processed
* constraints.
*
* @param mapping
*/
protected void processConstraintMapping(ConstraintMapping mapping)
{
Map<String, RoleInfo> mappings = (Map<String, RoleInfo>)_constraintMap.get(mapping.getPathSpec());
if (mappings == null)
{
mappings = new StringMap();
_constraintMap.put(mapping.getPathSpec(),mappings);
}
RoleInfo allMethodsRoleInfo = mappings.get(null);
if (allMethodsRoleInfo != null && allMethodsRoleInfo.isForbidden())
return;
if (mapping.getMethodOmissions() != null && mapping.getMethodOmissions().length > 0)
{
processConstraintMappingWithMethodOmissions(mapping, mappings);
return;
}
String httpMethod = mapping.getMethod();
RoleInfo roleInfo = mappings.get(httpMethod);
if (roleInfo == null)
{
roleInfo = new RoleInfo();
mappings.put(httpMethod,roleInfo);
if (allMethodsRoleInfo != null)
{
roleInfo.combine(allMethodsRoleInfo);
}
}
if (roleInfo.isForbidden())
return;
//add in info from the constraint
configureRoleInfo(roleInfo, mapping);
if (roleInfo.isForbidden())
{
if (httpMethod == null)
{
mappings.clear();
mappings.put(null,roleInfo);
}
}
else
{
//combine with any entry that covers all methods
if (httpMethod == null)
{
for (Map.Entry<String, RoleInfo> entry : mappings.entrySet())
{
if (entry.getKey() != null)
{
RoleInfo specific = entry.getValue();
specific.combine(roleInfo);
}
}
}
}
}
/* ------------------------------------------------------------ */
/** Constraints that name method omissions are dealt with differently.
* We create an entry in the mappings with key "method.omission". This entry
* is only ever combined with other omissions for the same method to produce a
* consolidated RoleInfo. Then, when we wish to find the relevant constraints for
* a given Request (in prepareConstraintInfo()), we consult 3 types of entries in
* the mappings: an entry that names the method of the Request specifically, an
* entry that names constraints that apply to all methods, entries of the form
* method.omission, where the method of the Request is not named in the omission.
* @param mapping
* @param mappings
*/
protected void processConstraintMappingWithMethodOmissions (ConstraintMapping mapping, Map<String, RoleInfo> mappings)
{
String[] omissions = mapping.getMethodOmissions();
for (String omission:omissions)
{
//for each method omission, see if there is already a RoleInfo for it in mappings
RoleInfo ri = mappings.get(omission+OMISSION_SUFFIX);
if (ri == null)
{
//if not, make one
ri = new RoleInfo();
mappings.put(omission+OMISSION_SUFFIX, ri);
}
//initialize RoleInfo or combine from ConstraintMapping
configureRoleInfo(ri, mapping);
}
}
/* ------------------------------------------------------------ */
/**
* Initialize or update the RoleInfo from the constraint
* @param ri
* @param mapping
*/
protected void configureRoleInfo (RoleInfo ri, ConstraintMapping mapping)
{
Constraint constraint = mapping.getConstraint();
boolean forbidden = constraint.isForbidden();
ri.setForbidden(forbidden);
//set up the data constraint (NOTE: must be done after setForbidden, as it nulls out the data constraint
//which we need in order to do combining of omissions in prepareConstraintInfo
UserDataConstraint userDataConstraint = UserDataConstraint.get(mapping.getConstraint().getDataConstraint());
ri.setUserDataConstraint(userDataConstraint);
//if forbidden, no point setting up roles
if (!ri.isForbidden())
{
//add in the roles
boolean checked = mapping.getConstraint().getAuthenticate();
ri.setChecked(checked);
if (ri.isChecked())
{
if (mapping.getConstraint().isAnyRole())
{
if (_strict)
{
// * means "all defined roles"
for (String role : _roles)
ri.addRole(role);
}
else
// * means any role
ri.setAnyRole(true);
}
else
{
String[] newRoles = mapping.getConstraint().getRoles();
for (String role : newRoles)
{
if (_strict &&!_roles.contains(role))
throw new IllegalArgumentException("Attempt to use undeclared role: " + role + ", known roles: " + _roles);
ri.addRole(role);
}
}
}
}
}
/* ------------------------------------------------------------ */
/**
* Find constraints that apply to the given path.
* In order to do this, we consult 3 different types of information stored in the mappings for each path - each mapping
* represents a merged set of user data constraints, roles etc -:
* <ol>
* <li>A mapping of an exact method name </li>
* <li>A mapping will null key that matches every method name</li>
* <li>Mappings with keys of the form "method.omission" that indicates it will match every method name EXCEPT that given</li>
* </ol>
*
* @see org.eclipse.jetty.security.SecurityHandler#prepareConstraintInfo(java.lang.String, org.eclipse.jetty.server.Request)
*/
protected Object prepareConstraintInfo(String pathInContext, Request request)
{
Map<String, RoleInfo> mappings = (Map<String, RoleInfo>)_constraintMap.match(pathInContext);
if (mappings != null)
{
String httpMethod = request.getMethod();
RoleInfo roleInfo = mappings.get(httpMethod);
if (roleInfo == null)
{
//No specific http-method names matched
List<RoleInfo> applicableConstraints = new ArrayList<RoleInfo>();
//Get info for constraint that matches all methods if it exists
RoleInfo all = mappings.get(null);
if (all != null)
applicableConstraints.add(all);
//Get info for constraints that name method omissions where target method name is not omitted
//(ie matches because target method is not omitted, hence considered covered by the constraint)
for (Entry<String, RoleInfo> entry: mappings.entrySet())
{
if (entry.getKey() != null && entry.getKey().contains(OMISSION_SUFFIX) && !(httpMethod+OMISSION_SUFFIX).equals(entry.getKey()))
applicableConstraints.add(entry.getValue());
}
if (applicableConstraints.size() == 1)
roleInfo = applicableConstraints.get(0);
else
{
roleInfo = new RoleInfo();
roleInfo.setUserDataConstraint(UserDataConstraint.None);
for (RoleInfo r:applicableConstraints)
roleInfo.combine(r);
}
}
return roleInfo;
}
return null;
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.security.SecurityHandler#checkUserDataPermissions(java.lang.String, org.eclipse.jetty.server.Request, org.eclipse.jetty.server.Response, java.lang.Object)
*/
protected boolean checkUserDataPermissions(String pathInContext, Request request, Response response, Object constraintInfo) throws IOException
{
if (constraintInfo == null)
return true;
RoleInfo roleInfo = (RoleInfo)constraintInfo;
if (roleInfo.isForbidden())
return false;
UserDataConstraint dataConstraint = roleInfo.getUserDataConstraint();
if (dataConstraint == null || dataConstraint == UserDataConstraint.None)
{
return true;
}
AbstractHttpConnection connection = AbstractHttpConnection.getCurrentConnection();
Connector connector = connection.getConnector();
if (dataConstraint == UserDataConstraint.Integral)
{
if (connector.isIntegral(request))
return true;
if (connector.getIntegralPort() > 0)
{
String scheme=connector.getIntegralScheme();
int port=connector.getIntegralPort();
String url = (HttpSchemes.HTTPS.equalsIgnoreCase(scheme) && port==443)
? "https://"+request.getServerName()+request.getRequestURI()
: scheme + "://" + request.getServerName() + ":" + port + request.getRequestURI();
if (request.getQueryString() != null)
url += "?" + request.getQueryString();
response.setContentLength(0);
response.sendRedirect(url);
}
else
response.sendError(Response.SC_FORBIDDEN,"!Integral");
request.setHandled(true);
return false;
}
else if (dataConstraint == UserDataConstraint.Confidential)
{
if (connector.isConfidential(request))
return true;
if (connector.getConfidentialPort() > 0)
{
String scheme=connector.getConfidentialScheme();
int port=connector.getConfidentialPort();
String url = (HttpSchemes.HTTPS.equalsIgnoreCase(scheme) && port==443)
? "https://"+request.getServerName()+request.getRequestURI()
: scheme + "://" + request.getServerName() + ":" + port + request.getRequestURI();
if (request.getQueryString() != null)
url += "?" + request.getQueryString();
response.setContentLength(0);
response.sendRedirect(url);
}
else
response.sendError(Response.SC_FORBIDDEN,"!Confidential");
request.setHandled(true);
return false;
}
else
{
throw new IllegalArgumentException("Invalid dataConstraint value: " + dataConstraint);
}
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.security.SecurityHandler#isAuthMandatory(org.eclipse.jetty.server.Request, org.eclipse.jetty.server.Response, java.lang.Object)
*/
protected boolean isAuthMandatory(Request baseRequest, Response base_response, Object constraintInfo)
{
if (constraintInfo == null)
{
return false;
}
return ((RoleInfo)constraintInfo).isChecked();
}
/* ------------------------------------------------------------ */
/**
* @see org.eclipse.jetty.security.SecurityHandler#checkWebResourcePermissions(java.lang.String, org.eclipse.jetty.server.Request, org.eclipse.jetty.server.Response, java.lang.Object, org.eclipse.jetty.server.UserIdentity)
*/
@Override
protected boolean checkWebResourcePermissions(String pathInContext, Request request, Response response, Object constraintInfo, UserIdentity userIdentity)
throws IOException
{
if (constraintInfo == null)
{
return true;
}
RoleInfo roleInfo = (RoleInfo)constraintInfo;
if (!roleInfo.isChecked())
{
return true;
}
if (roleInfo.isAnyRole() && request.getAuthType()!=null)
return true;
for (String role : roleInfo.getRoles())
{
if (userIdentity.isUserInRole(role, null))
return true;
}
return false;
}
/* ------------------------------------------------------------ */
@Override
public void dump(Appendable out,String indent) throws IOException
{
dumpThis(out);
dump(out,indent,
Collections.singleton(getLoginService()),
Collections.singleton(getIdentityService()),
Collections.singleton(getAuthenticator()),
Collections.singleton(_roles),
_constraintMap.entrySet(),
getBeans(),
TypeUtil.asList(getHandlers()));
}
}