# 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);
       }
    }