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