diff options
Diffstat (limited to 'jetty-security/src')
4 files changed, 784 insertions, 53 deletions
diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java index ec998dd131..eb9235ab42 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/ConstraintSecurityHandler.java @@ -19,15 +19,24 @@ 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 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.HttpStatus; import org.eclipse.jetty.http.PathMap; import org.eclipse.jetty.server.HttpChannel; @@ -36,18 +45,18 @@ 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 - * 2.4 compliant and pre-computes the constraint combinations for runtime + * 3.0 compliant and pre-computes the constraint combinations for runtime * efficiency. * */ public class ConstraintSecurityHandler extends SecurityHandler implements ConstraintAware { + private static final String OMISSION_SUFFIX = ".omission"; private static final String ALL_METHODS = "*"; private final List<ConstraintMapping> _constraintMappings= new CopyOnWriteArrayList<>(); private final Set<String> _roles = new CopyOnWriteArraySet<>(); @@ -55,6 +64,212 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr 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. */ @@ -140,8 +355,6 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr @Override public void setConstraintMappings(List<ConstraintMapping> constraintMappings, Set<String> roles) { - if (isStarted()) - throw new IllegalStateException("Started"); _constraintMappings.clear(); _constraintMappings.addAll(constraintMappings); @@ -160,6 +373,14 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } } setRoles(roles); + + if (isStarted()) + { + for (ConstraintMapping mapping : _constraintMappings) + { + processConstraintMapping(mapping); + } + } } /* ------------------------------------------------------------ */ @@ -172,9 +393,6 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr */ public void setRoles(Set<String> roles) { - if (isStarted()) - throw new IllegalStateException("Started"); - _roles.clear(); _roles.addAll(roles); } @@ -238,14 +456,24 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } super.doStart(); } - + + + /* ------------------------------------------------------------ */ @Override protected void doStop() throws Exception { super.doStop(); _constraintMap.clear(); } - + + + /* ------------------------------------------------------------ */ + /** + * Create and combine the constraint with the existing processed + * constraints. + * + * @param mapping + */ protected void processConstraintMapping(ConstraintMapping mapping) { Map<String, RoleInfo> mappings = _constraintMap.get(mapping.getPathSpec()); @@ -258,6 +486,12 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr if (allMethodsRoleInfo != null && allMethodsRoleInfo.isForbidden()) return; + if (mapping.getMethodOmissions() != null && mapping.getMethodOmissions().length > 0) + { + processConstraintMappingWithMethodOmissions(mapping, mappings); + return; + } + String httpMethod = mapping.getMethod(); if (httpMethod==null) httpMethod=ALL_METHODS; @@ -274,10 +508,10 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr if (roleInfo.isForbidden()) return; - Constraint constraint = mapping.getConstraint(); - boolean forbidden = constraint.isForbidden(); - roleInfo.setForbidden(forbidden); - if (forbidden) + //add in info from the constraint + configureRoleInfo(roleInfo, mapping); + + if (roleInfo.isForbidden()) { if (httpMethod.equals(ALL_METHODS)) { @@ -287,41 +521,12 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } else { - UserDataConstraint userDataConstraint = UserDataConstraint.get(constraint.getDataConstraint()); - roleInfo.setUserDataConstraint(userDataConstraint); - - boolean checked = constraint.getAuthenticate(); - roleInfo.setChecked(checked); - if (roleInfo.isChecked()) - { - if (constraint.isAnyRole()) - { - if (_strict) - { - // * means "all defined roles" - for (String role : _roles) - roleInfo.addRole(role); - } - else - // * means any role - roleInfo.setAnyRole(true); - } - else - { - String[] newRoles = constraint.getRoles(); - for (String role : newRoles) - { - if (_strict &&!_roles.contains(role)) - throw new IllegalArgumentException("Attempt to use undeclared role: " + role + ", known roles: " + _roles); - roleInfo.addRole(role); - } - } - } - if (httpMethod.equals(ALL_METHODS)) + //combine with any entry that covers all methods + if (httpMethod == null) { for (Map.Entry<String, RoleInfo> entry : mappings.entrySet()) { - if (!entry.getKey().equals(ALL_METHODS)) + if (entry.getKey() != null) { RoleInfo specific = entry.getValue(); specific.combine(roleInfo); @@ -331,17 +536,145 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } } + /* ------------------------------------------------------------ */ + /** 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) + */ @Override protected RoleInfo prepareConstraintInfo(String pathInContext, Request request) { - Map<String, RoleInfo> mappings = _constraintMap.match(pathInContext); + 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) - roleInfo = mappings.get(ALL_METHODS); + { + //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(ALL_METHODS); + 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; } @@ -396,7 +729,12 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr { return constraintInfo != null && ((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 @@ -435,4 +773,5 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr Collections.singleton(_roles), _constraintMap.entrySet()); } + } diff --git a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java index 7e7ec72d49..9e94282364 100644 --- a/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java +++ b/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/SessionAuthentication.java @@ -119,18 +119,25 @@ public class SessionAuthentication implements Authentication.User, Serializable, @Override public void sessionWillPassivate(HttpSessionEvent se) { + } @Override public void sessionDidActivate(HttpSessionEvent se) { if (_session==null) + { _session=se.getSession(); + } } @Override public void valueBound(HttpSessionBindingEvent event) { + if (_session==null) + { + _session=event.getSession(); + } } @Override diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java index 1d903c4ad0..75f0e62554 100644 --- a/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/ConstraintTest.java @@ -79,6 +79,8 @@ public class ConstraintTest _loginService.putUser("user",new Password("password")); _loginService.putUser("user2",new Password("password"), new String[] {"user"}); _loginService.putUser("admin",new Password("password"), new String[] {"user","administrator"}); + _loginService.putUser("user3", new Password("password"), new String[] {"foo"}); + _context.setContextPath("/ctx"); _server.setHandler(_context); @@ -190,17 +192,59 @@ public class ConstraintTest @Test public void testBasic() throws Exception { + + List<ConstraintMapping> list = new ArrayList<ConstraintMapping>(_security.getConstraintMappings()); + + Constraint constraint6 = new Constraint(); + constraint6.setAuthenticate(true); + constraint6.setName("omit POST and GET"); + constraint6.setRoles(new String[]{"user"}); + ConstraintMapping mapping6 = new ConstraintMapping(); + mapping6.setPathSpec("/omit/*"); + mapping6.setConstraint(constraint6); + mapping6.setMethodOmissions(new String[]{"GET", "HEAD"}); //requests for every method except GET and HEAD must be in role "user" + list.add(mapping6); + + Constraint constraint7 = new Constraint(); + constraint7.setAuthenticate(true); + constraint7.setName("non-omitted GET"); + constraint7.setRoles(new String[]{"administrator"}); + ConstraintMapping mapping7 = new ConstraintMapping(); + mapping7.setPathSpec("/omit/*"); + mapping7.setConstraint(constraint7); + mapping7.setMethod("GET"); //requests for GET must be in role "admin" + list.add(mapping7); + + Constraint constraint8 = new Constraint(); + constraint8.setAuthenticate(true); + constraint8.setName("non specific"); + constraint8.setRoles(new String[]{"foo"}); + ConstraintMapping mapping8 = new ConstraintMapping(); + mapping8.setPathSpec("/omit/*"); + mapping8.setConstraint(constraint8);//requests for all methods must be in role "foo" + list.add(mapping8); + + Set<String> knownRoles=new HashSet<String>(); + knownRoles.add("user"); + knownRoles.add("administrator"); + knownRoles.add("foo"); + + _security.setConstraintMappings(list, knownRoles); + + _security.setAuthenticator(new BasicAuthenticator()); _security.setStrict(false); _server.start(); String response; + /* response = _connector.getResponses("GET /ctx/noauth/info HTTP/1.0\r\n\r\n"); assertThat(response,startsWith("HTTP/1.1 200 OK")); - +*/ + response = _connector.getResponses("GET /ctx/forbid/info HTTP/1.0\r\n\r\n"); assertThat(response,startsWith("HTTP/1.1 403 Forbidden")); - + /* response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); assertThat(response,startsWith("HTTP/1.1 401 Unauthorized")); assertThat(response,containsString("WWW-Authenticate: basic realm=\"TestRealm\"")); @@ -215,8 +259,8 @@ public class ConstraintTest "Authorization: Basic " + B64Code.encode("user:password") + "\r\n" + "\r\n"); assertThat(response,startsWith("HTTP/1.1 200 OK")); - - +*/ +/* // test admin response = _connector.getResponses("GET /ctx/admin/info HTTP/1.0\r\n\r\n"); assertThat(response,startsWith("HTTP/1.1 401 Unauthorized")); @@ -242,7 +286,33 @@ public class ConstraintTest response = _connector.getResponses("GET /ctx/admin/relax/info HTTP/1.0\r\n\r\n"); assertThat(response,startsWith("HTTP/1.1 200 OK")); + + //check GET is in role administrator + response = _connector.getResponses("GET /ctx/omit/x HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("admin:password") + "\r\n" + + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + + //check POST is in role user + response = _connector.getResponses("POST /ctx/omit/x HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("user2:password") + "\r\n" + + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + + //check POST can be in role foo too + response = _connector.getResponses("POST /ctx/omit/x HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("user3:password") + "\r\n" + + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + + //check HEAD cannot be in role user + response = _connector.getResponses("HEAD /ctx/omit/x HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("user2:password") + "\r\n" + + "\r\n"); + assertTrue(response.startsWith("HTTP/1.1 200 OK"));*/ } + + @Test public void testFormDispatch() throws Exception @@ -853,7 +923,7 @@ public class ConstraintTest public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException { baseRequest.setHandled(true); - if (request.getAuthType()==null || "user".equals(request.getRemoteUser()) || request.isUserInRole("user")) + if (request.getAuthType()==null || "user".equals(request.getRemoteUser()) || request.isUserInRole("user") || request.isUserInRole("foo")) { response.setStatus(200); response.setContentType("text/plain; charset=UTF-8"); diff --git a/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java new file mode 100644 index 0000000000..ba12f1af57 --- /dev/null +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java @@ -0,0 +1,315 @@ +// +// ======================================================================== +// Copyright (c) 1995-2012 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 static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.security.authentication.BasicAuthenticator; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.LocalConnector; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.util.security.Password; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * @version $Revision: 1441 $ $Date: 2010-04-02 12:28:17 +0200 (Fri, 02 Apr 2010) $ + */ +public class SpecExampleConstraintTest +{ + private static final String TEST_REALM = "TestRealm"; + private static Server _server; + private static LocalConnector _connector; + private static SessionHandler _session; + private ConstraintSecurityHandler _security; + + @BeforeClass + public static void startServer() + { + _server = new Server(); + _connector = new LocalConnector(_server); + _server.setConnectors(new Connector[]{_connector}); + + ContextHandler _context = new ContextHandler(); + _session = new SessionHandler(); + + HashLoginService _loginService = new HashLoginService(TEST_REALM); + _loginService.putUser("fred",new Password("password")); + _loginService.putUser("harry",new Password("password"), new String[] {"HOMEOWNER"}); + _loginService.putUser("chris",new Password("password"), new String[] {"CONTRACTOR"}); + _loginService.putUser("steven", new Password("password"), new String[] {"SALESCLERK"}); + + + _context.setContextPath("/ctx"); + _server.setHandler(_context); + _context.setHandler(_session); + + _server.addBean(_loginService); + } + + @Before + public void setupSecurity() + { + _security = new ConstraintSecurityHandler(); + _session.setHandler(_security); + RequestHandler _handler = new RequestHandler(); + _security.setHandler(_handler); + + + /* + + <security-constraint> + <web-resource-collection> + <web-resource-name>precluded methods</web-resource-name> + <url-pattern>/*</url-pattern> + <url-pattern>/acme/wholesale/*</url-pattern> + <url-pattern>/acme/retail/*</url-pattern> + <http-method-exception>GET</http-method-exception> + <http-method-exception>POST</http-method-exception> + </web-resource-collection> + <auth-constraint/> + </security-constraint> + */ + + Constraint constraint0 = new Constraint(); + constraint0.setAuthenticate(true); + constraint0.setName("precluded methods"); + ConstraintMapping mapping0 = new ConstraintMapping(); + mapping0.setPathSpec("/*"); + mapping0.setConstraint(constraint0); + mapping0.setMethodOmissions(new String[]{"GET", "POST"}); + + ConstraintMapping mapping1 = new ConstraintMapping(); + mapping1.setPathSpec("/acme/wholesale/*"); + mapping1.setConstraint(constraint0); + mapping1.setMethodOmissions(new String[]{"GET", "POST"}); + + ConstraintMapping mapping2 = new ConstraintMapping(); + mapping2.setPathSpec("/acme/retail/*"); + mapping2.setConstraint(constraint0); + mapping2.setMethodOmissions(new String[]{"GET", "POST"}); + + /* + + <security-constraint> + <web-resource-collection> + <web-resource-name>wholesale</web-resource-name> + <url-pattern>/acme/wholesale/*</url-pattern> + <http-method>GET</http-method> + <http-method>PUT</http-method> + </web-resource-collection> + <auth-constraint> + <role-name>SALESCLERK</role-name> + </auth-constraint> + </security-constraint> + */ + Constraint constraint1 = new Constraint(); + constraint1.setAuthenticate(true); + constraint1.setName("wholesale"); + constraint1.setRoles(new String[]{"SALESCLERK"}); + ConstraintMapping mapping3 = new ConstraintMapping(); + mapping3.setPathSpec("/acme/wholesale/*"); + mapping3.setConstraint(constraint1); + mapping3.setMethod("GET"); + ConstraintMapping mapping4 = new ConstraintMapping(); + mapping4.setPathSpec("/acme/wholesale/*"); + mapping4.setConstraint(constraint1); + mapping4.setMethod("PUT"); + + /* + <security-constraint> + <web-resource-collection> + <web-resource-name>wholesale 2</web-resource-name> + <url-pattern>/acme/wholesale/*</url-pattern> + <http-method>GET</http-method> + <http-method>POST</http-method> + </web-resource-collection> + <auth-constraint> + <role-name>CONTRACTOR</role-name> + </auth-constraint> + <user-data-constraint> + <transport-guarantee>CONFIDENTIAL</transport-guarantee> + </user-data-constraint> + </security-constraint> + */ + Constraint constraint2 = new Constraint(); + constraint2.setAuthenticate(true); + constraint2.setName("wholesale 2"); + constraint2.setRoles(new String[]{"CONTRACTOR"}); + constraint2.setDataConstraint(Constraint.DC_CONFIDENTIAL); + ConstraintMapping mapping5 = new ConstraintMapping(); + mapping5.setPathSpec("/acme/wholesale/*"); + mapping5.setMethod("GET"); + mapping5.setConstraint(constraint2); + ConstraintMapping mapping6 = new ConstraintMapping(); + mapping6.setPathSpec("/acme/wholesale/*"); + mapping6.setMethod("POST"); + mapping6.setConstraint(constraint2); + + /* +<security-constraint> +<web-resource-collection> +<web-resource-name>retail</web-resource-name> +<url-pattern>/acme/retail/*</url-pattern> +<http-method>GET</http-method> +<http-method>POST</http-method> +</web-resource-collection> +<auth-constraint> +<role-name>CONTRACTOR</role-name> +<role-name>HOMEOWNER</role-name> +</auth-constraint> +</security-constraint> +*/ + Constraint constraint4 = new Constraint(); + constraint4.setName("retail"); + constraint4.setAuthenticate(true); + constraint4.setRoles(new String[]{"CONTRACTOR", "HOMEOWNER"}); + ConstraintMapping mapping7 = new ConstraintMapping(); + mapping7.setPathSpec("/acme/retail/*"); + mapping7.setMethod("GET"); + mapping7.setConstraint(constraint4); + ConstraintMapping mapping8 = new ConstraintMapping(); + mapping8.setPathSpec("/acme/retail/*"); + mapping8.setMethod("POST"); + mapping8.setConstraint(constraint4); + + + + + Set<String> knownRoles=new HashSet<String>(); + knownRoles.add("CONTRACTOR"); + knownRoles.add("HOMEOWNER"); + knownRoles.add("SALESCLERK"); + + _security.setConstraintMappings(Arrays.asList(new ConstraintMapping[] + { + mapping0, mapping1, mapping2, mapping3, mapping4, mapping5, mapping6, mapping7, mapping8 + }), knownRoles); + } + + @After + public void stopServer() throws Exception + { + if (_server.isRunning()) + { + _server.stop(); + _server.join(); + } + } + + + + @Test + public void testBasic() throws Exception + { + + _security.setAuthenticator(new BasicAuthenticator()); + _security.setStrict(false); + _server.start(); + + String response; + /* + /star all methods except GET/POST forbidden + /acme/wholesale/star all methods except GET/POST forbidden + /acme/retail/star all methods except GET/POST forbidden + /acme/wholesale/star GET must be in role CONTRACTOR or SALESCLERK + /acme/wholesale/star POST must be in role CONTRACTOR and confidential transport + /acme/retail/star GET must be in role CONTRACTOR or HOMEOWNER + /acme/retail/star POST must be in role CONTRACTOR or HOMEOWNER + */ + + //a user in role HOMEOWNER is forbidden HEAD request + response = _connector.getResponses("HEAD /ctx/index.html HTTP/1.0\r\n\r\n"); + assertTrue(response.startsWith("HTTP/1.1 403 Forbidden")); + + response = _connector.getResponses("HEAD /ctx/index.html HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "\r\n"); + assertThat(response,startsWith("HTTP/1.1 403 Forbidden")); + + response = _connector.getResponses("HEAD /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "\r\n"); + assertThat(response,startsWith("HTTP/1.1 403 Forbidden")); + + response = _connector.getResponses("HEAD /ctx/acme/retail/index.html HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "\r\n"); + assertThat(response,startsWith("HTTP/1.1 403 Forbidden")); + + //a user in role CONTRACTOR can do a GET + response = _connector.getResponses("GET /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("chris:password") + "\r\n" + + "\r\n"); + + assertThat(response,startsWith("HTTP/1.1 200 OK")); + + //a user in role CONTRACTOR can only do a post if confidential + response = _connector.getResponses("POST /ctx/acme/wholesale/index.html HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("chris:password") + "\r\n" + + "\r\n"); + assertThat(response,startsWith("HTTP/1.1 403 !")); + + //a user in role HOMEOWNER can do a GET + response = _connector.getResponses("GET /ctx/acme/retail/index.html HTTP/1.0\r\n" + + "Authorization: Basic " + B64Code.encode("harry:password") + "\r\n" + + "\r\n"); + assertThat(response,startsWith("HTTP/1.1 200 OK")); + } + + + private class RequestHandler extends AbstractHandler + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException + { + baseRequest.setHandled(true); + + response.setStatus(200); + response.setContentType("text/plain; charset=UTF-8"); + response.getWriter().println("URI="+request.getRequestURI()); + String user = request.getRemoteUser(); + response.getWriter().println("user="+user); + if (request.getParameter("test_parameter")!=null) + response.getWriter().println(request.getParameter("test_parameter")); + } + } + +} |