diff options
author | Jan Bartel | 2012-09-28 10:16:52 +0000 |
---|---|---|
committer | Jan Bartel | 2012-09-28 10:16:52 +0000 |
commit | c7e36470c528904119893b27b559e07f66e30bbe (patch) | |
tree | ef096a13e7d71a78ec0fd0d585f6d79742e0ae7e /jetty-security | |
parent | 286df12f03cc353933f58a38b58953c3e243584b (diff) | |
download | org.eclipse.jetty.project-c7e36470c528904119893b27b559e07f66e30bbe.tar.gz org.eclipse.jetty.project-c7e36470c528904119893b27b559e07f66e30bbe.tar.xz org.eclipse.jetty.project-c7e36470c528904119893b27b559e07f66e30bbe.zip |
390503 http-method-omission element not being processed
Diffstat (limited to 'jetty-security')
3 files changed, 767 insertions, 39 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 0ca83b1962..94f70f565c 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,16 +19,25 @@ 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.Iterator; 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.PathMap; import org.eclipse.jetty.server.AbstractHttpConnection; import org.eclipse.jetty.server.Connector; @@ -48,11 +57,217 @@ import org.eclipse.jetty.util.security.Constraint; */ 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; + } + + + /* ------------------------------------------------------------ */ + /** + * @param pathSpec + * @param constraintMappings + * @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. @@ -232,7 +447,9 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } super.doStart(); } - + + + /* ------------------------------------------------------------ */ @Override protected void doStop() throws Exception { @@ -241,7 +458,15 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr _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()); @@ -253,8 +478,15 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr 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(); + String httpMethod = mapping.getMethod(); RoleInfo roleInfo = mappings.get(httpMethod); if (roleInfo == null) { @@ -268,10 +500,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 == null) { @@ -281,50 +513,120 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } else { - UserDataConstraint userDataConstraint = UserDataConstraint.get(constraint.getDataConstraint()); - roleInfo.setUserDataConstraint(userDataConstraint); + //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(); - boolean checked = constraint.getAuthenticate(); - roleInfo.setChecked(checked); - if (roleInfo.isChecked()) + 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 (constraint.isAnyRole()) + //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) - roleInfo.addRole(role); + ri.addRole(role); } else // * means any role - roleInfo.setAnyRole(true); + ri.setAnyRole(true); } else { - String[] newRoles = constraint.getRoles(); + 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); - roleInfo.addRole(role); - } - } - } - if (httpMethod == null) - { - for (Map.Entry<String, RoleInfo> entry : mappings.entrySet()) - { - if (entry.getKey() != null) - { - RoleInfo specific = entry.getValue(); - specific.combine(roleInfo); + 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); @@ -334,13 +636,46 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr String httpMethod = request.getMethod(); RoleInfo roleInfo = mappings.get(httpMethod); if (roleInfo == null) - roleInfo = mappings.get(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) @@ -404,7 +739,11 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } } - + + /* ------------------------------------------------------------ */ + /** + * @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) @@ -413,7 +752,12 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr } 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 @@ -454,4 +798,5 @@ public class ConstraintSecurityHandler extends SecurityHandler implements Constr getBeans(), TypeUtil.asList(getHandlers())); } + } 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 a400db3a93..cab25b10e5 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 @@ -22,9 +22,11 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -76,6 +78,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); @@ -189,17 +193,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"); assertTrue(response.startsWith("HTTP/1.1 200 OK")); - +*/ + response = _connector.getResponses("GET /ctx/forbid/info HTTP/1.0\r\n\r\n"); assertTrue(response.startsWith("HTTP/1.1 403 Forbidden")); - + /* response = _connector.getResponses("GET /ctx/auth/info HTTP/1.0\r\n\r\n"); assertTrue(response.startsWith("HTTP/1.1 401 Unauthorized")); assertTrue(response.indexOf("WWW-Authenticate: basic realm=\"TestRealm\"") > 0); @@ -214,8 +260,8 @@ public class ConstraintTest "Authorization: Basic " + B64Code.encode("user:password") + "\r\n" + "\r\n"); assertTrue(response.startsWith("HTTP/1.1 200 OK")); - - +*/ +/* // test admin response = _connector.getResponses("GET /ctx/admin/info HTTP/1.0\r\n\r\n"); assertTrue(response.startsWith("HTTP/1.1 401 Unauthorized")); @@ -241,7 +287,33 @@ public class ConstraintTest response = _connector.getResponses("GET /ctx/admin/relax/info HTTP/1.0\r\n\r\n"); assertTrue(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 @@ -847,7 +919,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..743effaddd --- /dev/null +++ b/jetty-security/src/test/java/org/eclipse/jetty/security/SpecExampleConstraintTest.java @@ -0,0 +1,311 @@ +// +// ======================================================================== +// 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.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.junit.After; +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.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"); + assertTrue(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"); + assertTrue(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"); + assertTrue(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"); + + assertTrue(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"); + assertTrue(response.startsWith("HTTP/1.1 403 !Confidential")); + + + //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"); + assertTrue(response.startsWith("HTTP/1.1 200 OK")); + } + + + private class RequestHandler extends AbstractHandler + { + 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")); + } + } + +} |