View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.security;
20  
21  import java.io.IOException;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.concurrent.CopyOnWriteArrayList;
30  import java.util.concurrent.CopyOnWriteArraySet;
31  
32  import org.eclipse.jetty.http.HttpSchemes;
33  import org.eclipse.jetty.http.PathMap;
34  import org.eclipse.jetty.server.AbstractHttpConnection;
35  import org.eclipse.jetty.server.Connector;
36  import org.eclipse.jetty.server.Request;
37  import org.eclipse.jetty.server.Response;
38  import org.eclipse.jetty.server.UserIdentity;
39  import org.eclipse.jetty.util.StringMap;
40  import org.eclipse.jetty.util.TypeUtil;
41  import org.eclipse.jetty.util.security.Constraint;
42  
43  /* ------------------------------------------------------------ */
44  /**
45   * Handler to enforce SecurityConstraints. This implementation is servlet spec
46   * 2.4 compliant and precomputes the constraint combinations for runtime
47   * efficiency.
48   *
49   */
50  public class ConstraintSecurityHandler extends SecurityHandler implements ConstraintAware
51  {
52      private final List<ConstraintMapping> _constraintMappings= new CopyOnWriteArrayList<ConstraintMapping>();
53      private final Set<String> _roles = new CopyOnWriteArraySet<String>();
54      private final PathMap _constraintMap = new PathMap();
55      private boolean _strict = true;
56  
57      /* ------------------------------------------------------------ */
58      /** Get the strict mode.
59       * @return true if the security handler is running in strict mode.
60       */
61      public boolean isStrict()
62      {
63          return _strict;
64      }
65  
66      /* ------------------------------------------------------------ */
67      /** Set the strict mode of the security handler.
68       * <p>
69       * When in strict mode (the default), the full servlet specification
70       * will be implemented.
71       * If not in strict mode, some additional flexibility in configuration
72       * is allowed:<ul>
73       * <li>All users do not need to have a role defined in the deployment descriptor
74       * <li>The * role in a constraint applies to ANY role rather than all roles defined in
75       * the deployment descriptor.
76       * </ul>
77       *
78       * @param strict the strict to set
79       * @see #setRoles(Set)
80       * @see #setConstraintMappings(List, Set)
81       */
82      public void setStrict(boolean strict)
83      {
84          _strict = strict;
85      }
86  
87      /* ------------------------------------------------------------ */
88      /**
89       * @return Returns the constraintMappings.
90       */
91      public List<ConstraintMapping> getConstraintMappings()
92      {
93          return _constraintMappings;
94      }
95  
96      /* ------------------------------------------------------------ */
97      public Set<String> getRoles()
98      {
99          return _roles;
100     }
101 
102     /* ------------------------------------------------------------ */
103     /**
104      * Process the constraints following the combining rules in Servlet 3.0 EA
105      * spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
106      *
107      * @param constraintMappings
108      *            The constraintMappings to set, from which the set of known roles
109      *            is determined.
110      */
111     public void setConstraintMappings(List<ConstraintMapping> constraintMappings)
112     {
113         setConstraintMappings(constraintMappings,null);
114     }
115 
116     /**
117      * Process the constraints following the combining rules in Servlet 3.0 EA
118      * spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
119      *
120      * @param constraintMappings
121      *            The constraintMappings to set as array, from which the set of known roles
122      *            is determined.  Needed to retain API compatibility for 7.x
123      */
124     public void setConstraintMappings( ConstraintMapping[] constraintMappings )
125     {
126         setConstraintMappings( Arrays.asList(constraintMappings), null);
127     }
128 
129     /* ------------------------------------------------------------ */
130     /**
131      * Process the constraints following the combining rules in Servlet 3.0 EA
132      * spec section 13.7.1 Note that much of the logic is in the RoleInfo class.
133      *
134      * @param constraintMappings
135      *            The constraintMappings to set.
136      * @param roles The known roles (or null to determine them from the mappings)
137      */
138     public void setConstraintMappings(List<ConstraintMapping> constraintMappings, Set<String> roles)
139     {
140         if (isStarted())
141             throw new IllegalStateException("Started");
142         _constraintMappings.clear();
143         _constraintMappings.addAll(constraintMappings);
144 
145         if (roles==null)
146         {
147             roles = new HashSet<String>();
148             for (ConstraintMapping cm : constraintMappings)
149             {
150                 String[] cmr = cm.getConstraint().getRoles();
151                 if (cmr!=null)
152                 {
153                     for (String r : cmr)
154                         if (!"*".equals(r))
155                             roles.add(r);
156                 }
157             }
158         }
159         setRoles(roles);
160     }
161 
162     /* ------------------------------------------------------------ */
163     /**
164      * Set the known roles.
165      * This may be overridden by a subsequent call to {@link #setConstraintMappings(ConstraintMapping[])} or
166      * {@link #setConstraintMappings(List, Set)}.
167      * @see #setStrict(boolean)
168      * @param roles The known roles (or null to determine them from the mappings)
169      */
170     public void setRoles(Set<String> roles)
171     {
172         if (isStarted())
173             throw new IllegalStateException("Started");
174 
175         _roles.clear();
176         _roles.addAll(roles);
177     }
178 
179 
180 
181     /* ------------------------------------------------------------ */
182     /**
183      * @see org.eclipse.jetty.security.ConstraintAware#addConstraintMapping(org.eclipse.jetty.security.ConstraintMapping)
184      */
185     public void addConstraintMapping(ConstraintMapping mapping)
186     {
187         _constraintMappings.add(mapping);
188         if (mapping.getConstraint()!=null && mapping.getConstraint().getRoles()!=null)
189             for (String role :  mapping.getConstraint().getRoles())
190                 addRole(role);
191 
192         if (isStarted())
193         {
194             processConstraintMapping(mapping);
195         }
196     }
197 
198     /* ------------------------------------------------------------ */
199     /**
200      * @see org.eclipse.jetty.security.ConstraintAware#addRole(java.lang.String)
201      */
202     public void addRole(String role)
203     {
204         boolean modified = _roles.add(role);
205         if (isStarted() && modified && _strict)
206         {
207             // Add the new role to currently defined any role role infos
208             for (Map<String,RoleInfo> map : (Collection<Map<String,RoleInfo>>)_constraintMap.values())
209             {
210                 for (RoleInfo info : map.values())
211                 {
212                     if (info.isAnyRole())
213                         info.addRole(role);
214                 }
215             }
216         }
217     }
218 
219     /* ------------------------------------------------------------ */
220     /**
221      * @see org.eclipse.jetty.security.SecurityHandler#doStart()
222      */
223     @Override
224     protected void doStart() throws Exception
225     {
226         _constraintMap.clear();
227         if (_constraintMappings!=null)
228         {
229             for (ConstraintMapping mapping : _constraintMappings)
230             {
231                 processConstraintMapping(mapping);
232             }
233         }
234         super.doStart();
235     }
236 
237     @Override
238     protected void doStop() throws Exception
239     {
240         _constraintMap.clear();
241         _constraintMappings.clear();
242         _roles.clear();
243         super.doStop();
244     }
245 
246     protected void processConstraintMapping(ConstraintMapping mapping)
247     {
248         Map<String, RoleInfo> mappings = (Map<String, RoleInfo>)_constraintMap.get(mapping.getPathSpec());
249         if (mappings == null)
250         {
251             mappings = new StringMap();
252             _constraintMap.put(mapping.getPathSpec(),mappings);
253         }
254         RoleInfo allMethodsRoleInfo = mappings.get(null);
255         if (allMethodsRoleInfo != null && allMethodsRoleInfo.isForbidden())
256             return;
257 
258         String httpMethod = mapping.getMethod();
259         RoleInfo roleInfo = mappings.get(httpMethod);
260         if (roleInfo == null)
261         {
262             roleInfo = new RoleInfo();
263             mappings.put(httpMethod,roleInfo);
264             if (allMethodsRoleInfo != null)
265             {
266                 roleInfo.combine(allMethodsRoleInfo);
267             }
268         }
269         if (roleInfo.isForbidden())
270             return;
271 
272         Constraint constraint = mapping.getConstraint();
273         boolean forbidden = constraint.isForbidden();
274         roleInfo.setForbidden(forbidden);
275         if (forbidden)
276         {
277             if (httpMethod == null)
278             {
279                 mappings.clear();
280                 mappings.put(null,roleInfo);
281             }
282         }
283         else
284         {
285             UserDataConstraint userDataConstraint = UserDataConstraint.get(constraint.getDataConstraint());
286             roleInfo.setUserDataConstraint(userDataConstraint);
287 
288             boolean checked = constraint.getAuthenticate();
289             roleInfo.setChecked(checked);
290             if (roleInfo.isChecked())
291             {
292                 if (constraint.isAnyRole())
293                 {
294                     if (_strict)
295                     {
296                         // * means "all defined roles"
297                         for (String role : _roles)
298                             roleInfo.addRole(role);
299                     }
300                     else
301                         // * means any role
302                         roleInfo.setAnyRole(true);
303                 }
304                 else
305                 {
306                     String[] newRoles = constraint.getRoles();
307                     for (String role : newRoles)
308                     {
309                         if (_strict &&!_roles.contains(role))
310                             throw new IllegalArgumentException("Attempt to use undeclared role: " + role + ", known roles: " + _roles);
311                         roleInfo.addRole(role);
312                     }
313                 }
314             }
315             if (httpMethod == null)
316             {
317                 for (Map.Entry<String, RoleInfo> entry : mappings.entrySet())
318                 {
319                     if (entry.getKey() != null)
320                     {
321                         RoleInfo specific = entry.getValue();
322                         specific.combine(roleInfo);
323                     }
324                 }
325             }
326         }
327     }
328 
329     protected Object prepareConstraintInfo(String pathInContext, Request request)
330     {
331         Map<String, RoleInfo> mappings = (Map<String, RoleInfo>)_constraintMap.match(pathInContext);
332 
333         if (mappings != null)
334         {
335             String httpMethod = request.getMethod();
336             RoleInfo roleInfo = mappings.get(httpMethod);
337             if (roleInfo == null)
338                 roleInfo = mappings.get(null);
339             return roleInfo;
340         }
341 
342         return null;
343     }
344 
345     protected boolean checkUserDataPermissions(String pathInContext, Request request, Response response, Object constraintInfo) throws IOException
346     {
347         if (constraintInfo == null)
348             return true;
349 
350         RoleInfo roleInfo = (RoleInfo)constraintInfo;
351         if (roleInfo.isForbidden())
352             return false;
353 
354 
355         UserDataConstraint dataConstraint = roleInfo.getUserDataConstraint();
356         if (dataConstraint == null || dataConstraint == UserDataConstraint.None)
357         {
358             return true;
359         }
360         AbstractHttpConnection connection = AbstractHttpConnection.getCurrentConnection();
361         Connector connector = connection.getConnector();
362 
363         if (dataConstraint == UserDataConstraint.Integral)
364         {
365             if (connector.isIntegral(request))
366                 return true;
367             if (connector.getIntegralPort() > 0)
368             {
369                 String scheme=connector.getIntegralScheme();
370                 int port=connector.getIntegralPort();
371                 String url = (HttpSchemes.HTTPS.equalsIgnoreCase(scheme) && port==443)
372                     ? "https://"+request.getServerName()+request.getRequestURI()
373                     : scheme + "://" + request.getServerName() + ":" + port + request.getRequestURI();
374                 if (request.getQueryString() != null)
375                     url += "?" + request.getQueryString();
376                 response.setContentLength(0);
377                 response.sendRedirect(url);
378             }
379             else
380                 response.sendError(Response.SC_FORBIDDEN,"!Integral");
381 
382             request.setHandled(true);
383             return false;
384         }
385         else if (dataConstraint == UserDataConstraint.Confidential)
386         {
387             if (connector.isConfidential(request))
388                 return true;
389 
390             if (connector.getConfidentialPort() > 0)
391             {
392                 String scheme=connector.getConfidentialScheme();
393                 int port=connector.getConfidentialPort();
394                 String url = (HttpSchemes.HTTPS.equalsIgnoreCase(scheme) && port==443)
395                     ? "https://"+request.getServerName()+request.getRequestURI()
396                     : scheme + "://" + request.getServerName() + ":" + port + request.getRequestURI();                    
397                 if (request.getQueryString() != null)
398                     url += "?" + request.getQueryString();
399                 response.setContentLength(0);
400                 response.sendRedirect(url);
401             }
402             else
403                 response.sendError(Response.SC_FORBIDDEN,"!Confidential");
404 
405             request.setHandled(true);
406             return false;
407         }
408         else
409         {
410             throw new IllegalArgumentException("Invalid dataConstraint value: " + dataConstraint);
411         }
412 
413     }
414 
415     protected boolean isAuthMandatory(Request baseRequest, Response base_response, Object constraintInfo)
416     {
417         if (constraintInfo == null)
418         {
419             return false;
420         }
421         return ((RoleInfo)constraintInfo).isChecked();
422     }
423 
424     @Override
425     protected boolean checkWebResourcePermissions(String pathInContext, Request request, Response response, Object constraintInfo, UserIdentity userIdentity)
426             throws IOException
427     {
428         if (constraintInfo == null)
429         {
430             return true;
431         }
432         RoleInfo roleInfo = (RoleInfo)constraintInfo;
433 
434         if (!roleInfo.isChecked())
435         {
436             return true;
437         }
438 
439         if (roleInfo.isAnyRole() && request.getAuthType()!=null)
440             return true;
441 
442         for (String role : roleInfo.getRoles())
443         {
444             if (userIdentity.isUserInRole(role, null))
445                 return true;
446         }
447         return false;
448     }
449 
450     /* ------------------------------------------------------------ */
451     @Override
452     public void dump(Appendable out,String indent) throws IOException
453     {
454         dumpThis(out);
455         dump(out,indent,
456                 Collections.singleton(getLoginService()),
457                 Collections.singleton(getIdentityService()),
458                 Collections.singleton(getAuthenticator()),
459                 Collections.singleton(_roles),
460                 _constraintMap.entrySet(),
461                 getBeans(),
462                 TypeUtil.asList(getHandlers()));
463     }
464 }