WebServiceAuth.java

/*
** Module   : WebServiceAuth.java
** Abstract : Base class for authenticating and authorizing a web request.
**
** Copyright (c) 2022-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ------------------------------------Description-------------------------------------------
** 001 CA  20220404 Created the first version.
** 002 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 003 SBI 20230602 Changed string presentations of the web services that each service path would be represented
**                  by the absolute path from the web service root.
** 004 GBB 20230825 SecurityManager legacy web methods moved to LegacyWebSecurityManager.
** 005 RAA 20240604 Avoided a null contextPath when doing authorization.
** 006 GBB 20240826 Moving FileSystem to osresource package.
*/ 
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.main;

import java.nio.file.*;

import javax.servlet.http.*;

import org.eclipse.jetty.http.*;

import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.osresource.FileSystem;
import com.goldencode.p2j.util.logging.CentralLogger;

/**
 * Base class for authenticating and authorizing a web request.
 */
abstract class WebServiceAuth
{
   /** Logger. */
   protected static final CentralLogger LOG = CentralLogger.get(WebServiceAuth.class.getName());

   /** The name of the header holding the FWD authentication token. */
   private static final String FWD_SESSION_ID_HEADER_NAME = "FwdSessionId";

   /** 
    * When <code>true</code>, the login service must be used to authenticate. Otherwise, each service call must 
    * have the authentication data in its request.
    */
   protected final boolean loginApiAuth;
   
   /** The timeout for the created context. */
   protected final int timeout;

   /** The cached {@link LegacyWebSecurityManager} instance. */
   protected final LegacyWebSecurityManager wsm;

   /** The web service type (REST, SOAP, WEBHANDLER). */
   private final String type;

   /** The resource plugin used to perform the authorization. */
   private final WebServiceResource webServiceResource;

   /**
    * Create a web service authentication and authorization with the specified details.
    *  
    * @param    type
    *           The web service type (REST, SOAP, WEBHANDLER).
    * @param    loginApiAuth
    *           Flag indicating if there is an explicit login API to be used.
    * @param    timeout
    *           The context timeout.
    */
   public WebServiceAuth(String type, boolean loginApiAuth, int timeout)
   {
      this.type = type;
      this.loginApiAuth = loginApiAuth;
      this.timeout = timeout;
      SecurityManager sm = SecurityManager.getInstance();
      this.wsm = sm.legacyWebSm;
      this.webServiceResource = (WebServiceResource) sm.getPluginInstance("webservice");
   }

   /**
    * Perform the actual login, which will create the FWD context associated with this web request.
    * 
    * @param    request
    *           The HTTP request.
    *           
    * @return   The authentication token, or <code>null</code> if the authentication failed.
    */
   protected abstract String login(HttpServletRequest request);

   /**
    * Authenticate this request, but do not create the FWD context.
    * 
    * @param    request
    *           The HTTP request.
    *           
    * @return   <code>true</code> if the request contains valid credentials.
    */
   protected abstract boolean authenticate(HttpServletRequest request);

   /**
    * Perform the logout operation.  This will destroy the context associated with the specified token.
    * 
    * @param    token
    *           The authentication token
    *           
    * @return   <code>true</code> if the context was destroyed.
    */
   protected boolean logout(String token)
   {
      if (token == null)
      {
         return false;
      }
      
      boolean logout = wsm.destroyWebRequestContext(token);

      return logout;
   }
   
   /**
    * Authenticate the web request.  If the {@link #loginApiAuth} is set, the authorization token will be read
    * from the {@value #FWD_SESSION_ID_HEADER_NAME} header, and will return <code>true</code> if the context
    * exists for this token.
    * <p>
    * Otherwise, the request is authenticated.
    * 
    * @param    request
    *           The HTTP request.
    * @param    response
    *           The HTTP response.
    *           
    * @return   <code>true</code> if the request could be authenticated.
    */
   protected final boolean authenticate(HttpServletRequest request, HttpServletResponse response)
   {
      if (!loginApiAuth)
      {
         // if we reach this point, it means each request must the authentication details; the context is not
         // created here, but when the authorization is performed
         boolean auth = authenticate(request);
         if (!auth)
         {
            response.setStatus(HttpStatus.UNAUTHORIZED_401);
         }
         
         return auth;
      }
      
      // otherwise, the token must be supplied in the request
      String token = getAuthorizationToken(request);
      
      if (token == null || !wsm.hasWebRequestContext(token)) 
      {
         response.setStatus(HttpStatus.FORBIDDEN_403);
         return false;
      }
      
      return true;
   }

   /**
    * Authorize this web request.  If the {@link #loginApiAuth} flag is not set, an {@link #login} is first
    * tried to authenticate the request, after which the authorization will be checked for the created context.
    * <p>
    * If the request can't be authenticated, the response status code will be set to 
    * {@link HttpStatus#UNAUTHORIZED_401}.
    * <p>
    * If the context is not authorized, the response status code will be set to 
    * {@link HttpStatus#FORBIDDEN_403}.
    * 
    * @param    target
    *           The target path.
    * @param    request
    *           The HTTP request.
    * @param    response
    *           The HTTP response.
    *           
    * @return   The authentication token, or <code>null</code> if the request can't be authenticated or
    *           authorized.
    */
   protected final String authorize(String target, HttpServletRequest request, HttpServletResponse response)
   {
      String token = null;
      
      if (!loginApiAuth)
      {
         // attempt an actual login, which (if authorized) will create the token and the FWD context, too.
         token = login(request);
         
         if (token == null)
         {
            response.setStatus(HttpStatus.UNAUTHORIZED_401);
            return null;
         }
      }
      else
      {
         // the token must be specified for each request
         token = getAuthorizationToken(request);
      }
      
      if (token == null || !wsm.hasWebRequestContext(token))
      {
         response.setStatus(HttpStatus.FORBIDDEN_403);
         return null;
      }
      
      
      boolean res = wsm.checkAuthorization(token, () ->
      {
         // the syntax is:
         // REST:HTTP-METHOD:/path/to/service
         // WEBHANDLER:HTTP-METHOD:/path/to/service
         // SOAP:HTTP-METHOD:/path/to/service:namespace/binding/operation
         String contextPath = request.getContextPath();
         if (contextPath == null)
         {
            contextPath = "";
         }
         
         String instance = type.toUpperCase() + ":" + 
                           request.getMethod().toUpperCase() + ":" + 
                           Paths.get(contextPath, target)
                           .normalize()
                           .toString()
                           .replaceAll(FileSystem.ANY_PATH_SEPARATOR, "/");
         return webServiceResource.isAllowed(instance);
      });
      
      if (!res)
      {
         if (!loginApiAuth)
         {
            logout(token);
         }
         
         response.setStatus(HttpStatus.FORBIDDEN_403);
         
         return null;
      }
      
      return token;
   }
   
   /**
    * Read the authorization token from the HTTP request's {@value #FWD_SESSION_ID_HEADER_NAME} header.
    * 
    * @param    request
    *           The HTTP response.
    * 
    * @return   The authorization token.
    */
   protected String getAuthorizationToken(HttpServletRequest request)
   {
      return request.getHeader(FWD_SESSION_ID_HEADER_NAME);
   }

   /**
    * Set the authorization token to the HTTP response, in the {@value #FWD_SESSION_ID_HEADER_NAME} header.
    * 
    * @param    token
    *           The authorization token.
    * @param    response
    *           The HTTP response.
    */
   protected void setAuthorizationToken(String token, HttpServletResponse response)
   {
      response.setHeader(FWD_SESSION_ID_HEADER_NAME, token);
   }
}