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