View Javadoc

1   // ========================================================================
2   // Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.jetty.security;
16  
17  import java.io.IOException;
18  import java.security.Principal;
19  import java.util.Map;
20  
21  import javax.servlet.ServletException;
22  import javax.servlet.http.HttpServletRequest;
23  import javax.servlet.http.HttpServletResponse;
24  
25  import org.mortbay.jetty.Connector;
26  import org.mortbay.jetty.HttpConnection;
27  import org.mortbay.jetty.Request;
28  import org.mortbay.jetty.Response;
29  import org.mortbay.jetty.handler.HandlerWrapper;
30  import org.mortbay.jetty.servlet.PathMap;
31  import org.mortbay.log.Log;
32  import org.mortbay.util.LazyList;
33  import org.mortbay.util.StringUtil;
34  
35  
36  /* ------------------------------------------------------------ */
37  /** Handler to enforce SecurityConstraints.
38   *
39   * @author Greg Wilkins (gregw)
40   */
41  public class SecurityHandler extends HandlerWrapper
42  {   
43      /* ------------------------------------------------------------ */
44      private String _authMethod=Constraint.__BASIC_AUTH;
45      private UserRealm _userRealm;
46      private ConstraintMapping[] _constraintMappings;
47      private PathMap _constraintMap=new PathMap();
48      private Authenticator _authenticator;
49      private NotChecked _notChecked=new NotChecked();
50      private boolean _checkWelcomeFiles=false;
51      
52  
53      /* ------------------------------------------------------------ */
54      /**
55       * @return Returns the authenticator.
56       */
57      public Authenticator getAuthenticator()
58      {
59          return _authenticator;
60      }
61      
62      /* ------------------------------------------------------------ */
63      /**
64       * @param authenticator The authenticator to set.
65       */
66      public void setAuthenticator(Authenticator authenticator)
67      {
68          _authenticator = authenticator;
69      }
70      
71      /* ------------------------------------------------------------ */
72      /**
73       * @return Returns the userRealm.
74       */
75      public UserRealm getUserRealm()
76      {
77          return _userRealm;
78      }
79      
80      /* ------------------------------------------------------------ */
81      /**
82       * @param userRealm The userRealm to set.
83       */
84      public void setUserRealm(UserRealm userRealm)
85      {
86          _userRealm = userRealm;
87      }
88      
89      /* ------------------------------------------------------------ */
90      /**
91       * @return Returns the contraintMappings.
92       */
93      public ConstraintMapping[] getConstraintMappings()
94      {
95          return _constraintMappings;
96      }
97      
98      /* ------------------------------------------------------------ */
99      /**
100      * @param contraintMappings The contraintMappings to set.
101      */
102     public void setConstraintMappings(ConstraintMapping[] constraintMappings)
103     {
104         _constraintMappings=constraintMappings;
105         if (_constraintMappings!=null)
106         {
107             this._constraintMappings = constraintMappings;
108             _constraintMap.clear();
109             
110             for (int i=0;i<_constraintMappings.length;i++)
111             {
112                 Object mappings = _constraintMap.get(_constraintMappings[i].getPathSpec());
113                 mappings=LazyList.add(mappings, _constraintMappings[i]);
114                 _constraintMap.put(_constraintMappings[i].getPathSpec(),mappings);
115             }
116         }
117     }
118     
119     /* ------------------------------------------------------------ */
120     public String getAuthMethod()
121     {
122         return _authMethod;
123     }
124     
125     /* ------------------------------------------------------------ */
126     public void setAuthMethod(String method)
127     {
128         if (isStarted() && _authMethod!=null && !_authMethod.equals(method))
129             throw new IllegalStateException("Handler started");
130         _authMethod = method;
131     }
132 
133     /* ------------------------------------------------------------ */
134     public boolean hasConstraints() 
135     {
136         return _constraintMappings != null && _constraintMappings.length > 0;
137     }
138 
139     /* ------------------------------------------------------------ */
140     /**
141      * @return True if forwards to welcome files are authenticated
142      */
143     public boolean isCheckWelcomeFiles()
144     {
145         return _checkWelcomeFiles;
146     }
147 
148     /* ------------------------------------------------------------ */
149     /**
150      * @param authenticateWelcomeFiles True if forwards to welcome files are authenticated
151      */
152     public void setCheckWelcomeFiles(boolean authenticateWelcomeFiles)
153     {
154         _checkWelcomeFiles=authenticateWelcomeFiles;
155     }
156     /* ------------------------------------------------------------ */
157     public void doStart()
158         throws Exception
159     {
160         if (_authenticator==null)
161         {
162             // Find out the Authenticator.
163             if (Constraint.__BASIC_AUTH.equalsIgnoreCase(_authMethod))
164                 _authenticator=new BasicAuthenticator();
165             else if (Constraint.__DIGEST_AUTH.equalsIgnoreCase(_authMethod))
166                 _authenticator=new DigestAuthenticator();
167             else if (Constraint.__CERT_AUTH.equalsIgnoreCase(_authMethod))
168                _authenticator=new ClientCertAuthenticator();
169             else if (Constraint.__FORM_AUTH.equalsIgnoreCase(_authMethod))
170                 _authenticator=new FormAuthenticator();
171             else
172                 Log.warn("Unknown Authentication method:"+_authMethod);
173         }
174         
175         super.doStart();
176     }
177     
178 
179     /* ------------------------------------------------------------ */
180     /* 
181      * @see org.mortbay.jetty.Handler#handle(java.lang.String, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, int)
182      */
183     public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException, ServletException 
184     {
185         Request base_request = (request instanceof Request) ? (Request)request:HttpConnection.getCurrentConnection().getRequest();
186         Response base_response = (response instanceof Response) ? (Response)response:HttpConnection.getCurrentConnection().getResponse();
187         UserRealm old_realm = base_request.getUserRealm();
188         try
189         {
190             base_request.setUserRealm(getUserRealm());
191             if (dispatch==REQUEST && !checkSecurityConstraints(target,base_request,base_response))
192             {
193                 base_request.setHandled(true);
194                 return;
195             }
196             
197             if (dispatch==FORWARD && _checkWelcomeFiles && request.getAttribute("org.mortbay.jetty.welcome")!=null)
198             {
199                 request.removeAttribute("org.mortbay.jetty.welcome");
200                 if (!checkSecurityConstraints(target,base_request,base_response))
201                 {
202                     base_request.setHandled(true);
203                     return;
204                 }
205             }
206                     
207                     
208             if (_authenticator instanceof FormAuthenticator && target.endsWith(FormAuthenticator.__J_SECURITY_CHECK))
209             {
210                 _authenticator.authenticate(getUserRealm(),target,base_request,base_response);
211                 base_request.setHandled(true);
212                 return;
213             }
214             
215             if (getHandler()!=null)
216                 getHandler().handle(target, request, response, dispatch);
217         }
218         finally
219         {
220             if (_userRealm!=null)
221             {
222                 if (dispatch==REQUEST)
223                 {
224                     _userRealm.disassociate(base_request.getUserPrincipal());
225                 }
226             }   
227             base_request.setUserRealm(old_realm);
228         }
229     }
230     
231 
232     /* ------------------------------------------------------------ */
233     public boolean checkSecurityConstraints(
234         String pathInContext,
235         Request request,
236         Response response)
237         throws IOException
238     {
239         Object mapping_entries= _constraintMap.getLazyMatches(pathInContext);
240         String pattern=null;
241         Object constraints= null;
242         
243         // for each path match
244         // Add only constraints that have the correct method
245         // break if the matching pattern changes.  This allows only
246         // constraints with matching pattern and method to be combined.
247         if (mapping_entries!=null)
248         {
249             loop: for (int m=0;m<LazyList.size(mapping_entries); m++)
250             {
251                 Map.Entry entry= (Map.Entry)LazyList.get(mapping_entries,m);
252                 Object mappings= entry.getValue();
253                 String path_spec=(String)entry.getKey();
254                 
255                 for (int c=0;c<LazyList.size(mappings);c++)
256                 {
257                     ConstraintMapping mapping=(ConstraintMapping)LazyList.get(mappings,c);
258                     if (mapping.getMethod()!=null && !mapping.getMethod().equalsIgnoreCase(request.getMethod()))
259                         continue;
260                     
261                     if (pattern!=null && !pattern.equals(path_spec))
262                         break loop;
263                     
264                     pattern=path_spec;	
265                     constraints= LazyList.add(constraints, mapping.getConstraint());
266                 }
267             }
268         
269             return check(constraints,_authenticator,_userRealm,pathInContext,request,response);
270         }
271         
272         request.setUserPrincipal(_notChecked);
273         return true;
274     }
275     
276 
277     /* ------------------------------------------------------------ */
278     /** Check security contraints
279      * @param constraints 
280      * @param authenticator 
281      * @param realm 
282      * @param pathInContext 
283      * @param request 
284      * @param response 
285      * @return false if the request has failed a security constraint or the authenticator has already sent a response.
286      * @exception IOException 
287      */
288     private boolean check(
289         Object constraints,
290         Authenticator authenticator,
291         UserRealm realm,
292         String pathInContext,
293         Request request,
294         Response response)
295         throws IOException
296     {
297         // Combine data and auth constraints
298         int dataConstraint= Constraint.DC_NONE;
299         Object roles= null;
300         boolean unauthenticated= false;
301         boolean forbidden= false;
302 
303         for (int c= 0; c < LazyList.size(constraints); c++)
304         {
305             Constraint sc= (Constraint)LazyList.get(constraints,c);
306 
307             // Combine data constraints.
308             if (dataConstraint > Constraint.DC_UNSET && sc.hasDataConstraint())
309             {
310                 if (sc.getDataConstraint() > dataConstraint)
311                     dataConstraint= sc.getDataConstraint();
312             }
313             else
314                 dataConstraint= Constraint.DC_UNSET; // ignore all other data constraints
315 
316             // Combine auth constraints.
317             if (!unauthenticated && !forbidden)
318             {
319                 if (sc.getAuthenticate())
320                 {
321                     if (sc.isAnyRole())
322                     {
323                         roles= Constraint.ANY_ROLE;
324                     }
325                     else
326                     {
327                         String[] scr= sc.getRoles();
328                         if (scr == null || scr.length == 0)
329                         {
330                             forbidden= true;
331                             break;
332                         }
333                         else
334                         {
335                             // TODO - this looks inefficient!
336                             if (roles != Constraint.ANY_ROLE)
337                             {
338                                 for (int r=scr.length;r-->0;)
339                                     roles= LazyList.add(roles, scr[r]);
340                             }
341                         }
342                     }
343                 }
344                 else
345                     unauthenticated= true;
346             }
347         }
348 
349         // Does this forbid everything?
350         if (forbidden && 
351             (!(authenticator instanceof FormAuthenticator) || 
352             !((FormAuthenticator)authenticator).isLoginOrErrorPage(pathInContext)))
353         {
354 
355             response.sendError(HttpServletResponse.SC_FORBIDDEN);
356             return false;
357         }
358 
359         // Handle data constraint
360         if (dataConstraint > Constraint.DC_NONE)
361         {
362             HttpConnection connection = HttpConnection.getCurrentConnection();
363             Connector connector = connection.getConnector();
364             
365             switch (dataConstraint)
366             {
367                 case Constraint.DC_INTEGRAL :
368                     if (connector.isIntegral(request))
369                         break;
370                     if (connector.getConfidentialPort() > 0)
371                     {
372                         String url=
373                             connector.getIntegralScheme()
374                                 + "://"
375                                 + request.getServerName()
376                                 + ":"
377                                 + connector.getIntegralPort()
378                                 + request.getRequestURI();
379                         if (request.getQueryString() != null)
380                             url += "?" + request.getQueryString();
381                         response.setContentLength(0);
382                         response.sendRedirect(response.encodeRedirectURL(url));
383                     }
384                     else
385                         response.sendError(Response.SC_FORBIDDEN,null);
386                     return false;
387                 case Constraint.DC_CONFIDENTIAL :
388                     if (connector.isConfidential(request))
389                         break;
390 
391                     if (connector.getConfidentialPort() > 0)
392                     {
393                         String url=
394                             connector.getConfidentialScheme()
395                                 + "://"
396                                 + request.getServerName()
397                                 + ":"
398                                 + connector.getConfidentialPort()
399                                 + request.getRequestURI();
400                         if (request.getQueryString() != null)
401                             url += "?" + request.getQueryString();
402 
403                         response.setContentLength(0);
404                         response.sendRedirect(response.encodeRedirectURL(url));
405                     }
406                     else
407                         response.sendError(Response.SC_FORBIDDEN,null);
408                     return false;
409 
410                 default :
411                     response.sendError(Response.SC_FORBIDDEN,null);
412                     return false;
413             }
414         }
415 
416         // Does it fail a role check?
417         if (!unauthenticated && roles != null)
418         {
419             if (realm == null)
420             {
421                 Log.warn("Request "+request.getRequestURI()+" failed - no realm");
422                 response.sendError(Response.SC_INTERNAL_SERVER_ERROR,"No realm");
423                 return false;
424             }
425 
426             Principal user= null;
427 
428             // Handle pre-authenticated request
429             if (request.getAuthType() != null && request.getRemoteUser() != null)
430             {
431                 // TODO - is this still needed???
432                 user= request.getUserPrincipal();
433                 if (user == null)
434                     user= realm.authenticate(request.getRemoteUser(), null, request);
435                 if (user == null && authenticator != null)
436                     user= authenticator.authenticate(realm, pathInContext, request, response);
437             }
438             else if (authenticator != null)
439             {
440                 // User authenticator.
441                 user= authenticator.authenticate(realm, pathInContext, request, response);
442             }
443             else
444             {
445                 // don't know how authenticate
446                 Log.warn("Mis-configured Authenticator for " + request.getRequestURI());
447                 response.sendError(Response.SC_INTERNAL_SERVER_ERROR,"Configuration error");
448             }
449 
450             // If we still did not get a user
451             if (user == null)
452                 return false; // Auth challenge or redirection already sent
453             else if (user == __NOBODY)
454                 return true; // The Nobody user indicates authentication in transit.
455 
456             if (roles != Constraint.ANY_ROLE)
457             {
458                 boolean inRole= false;
459                 for (int r= LazyList.size(roles); r-- > 0;)
460                 {
461                     if (realm.isUserInRole(user, (String)LazyList.get(roles, r)))
462                     {
463                         inRole= true;
464                         break;
465                     }
466                 }
467 
468                 if (!inRole)
469                 {
470                     Log.warn("AUTH FAILURE: incorrect role for " + StringUtil.printable(user.getName()));
471                     /* if ("BASIC".equalsIgnoreCase(authenticator.getAuthMethod()))
472                          ((BasicAuthenticator)authenticator).sendChallenge(realm, response);
473                     else for TCK */
474                     response.sendError(Response.SC_FORBIDDEN,"User not in required role");
475                     return false; // role failed.
476                 }
477             }
478         }
479         else
480         {
481             request.setUserPrincipal(_notChecked);
482         }
483 
484         return true;
485     }
486 
487     public static Principal __NO_USER = new Principal()
488     {
489         public String getName()
490         {
491             return null;
492         }
493         public String toString()
494         {
495             return "No User";
496         }
497     };
498     
499     public class NotChecked implements Principal
500     {
501         public String getName()
502         {
503             return null;
504         }
505         public String toString()
506         {
507             return "NOT CHECKED";
508         }
509         public SecurityHandler getSecurityHandler()
510         {
511             return SecurityHandler.this;
512         }
513     };
514 
515     /* ------------------------------------------------------------ */
516     /* ------------------------------------------------------------ */
517     /* ------------------------------------------------------------ */
518     /** Nobody user.
519      * The Nobody UserPrincipal is used to indicate a partial state of
520      * authentication. A request with a Nobody UserPrincipal will be allowed
521      * past all authentication constraints - but will not be considered an
522      * authenticated request.  It can be used by Authenticators such as
523      * FormAuthenticator to allow access to logon and error pages within an
524      * authenticated URI tree.
525      */
526     public static Principal __NOBODY = new Principal()
527     {
528         public String getName()
529         {
530             return "Nobody";
531         }
532         
533         public String toString()
534         {
535             return getName();
536         }
537     };
538 }
539