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(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(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