# LegacyWebSecurityManager.java

    /*
     ** Module   : LegacyWebSecurityManager.java
     ** Abstract : Security methods supporting legacy web (SOAP, REST, WebServiceHandler).
     **
     ** Copyright (c) 2023-2024, Golden Code Development Corporation.
     **
     ** -#- -I- --Date-- ------------------------------------Description-------------------------------------------
     ** 001 GBB 20230825 Initial version.
     ** 002 RAA 20240604 Added an executeInContext method with useInitial parameter.
     */
    /*
     ** 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.security;

    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;

    import java.util.*;
    import java.util.concurrent.*;
    import java.util.function.*;
    import java.util.logging.*;

    import static com.goldencode.p2j.security.SecurityManager.*;

    /**
     ** Wrapper class for all method related to legacy services.
     * {@link com.goldencode.p2j.main.WebServiceHandler}
     * {@link com.goldencode.p2j.soap.SoapHandler}
     * {@link com.goldencode.p2j.rest.RestHandler}
     * <p>
     * Using it through {@link SecurityManager#legacyWebSm} allows resetting SecurityManager instance.
     */
    public class LegacyWebSecurityManager
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(LegacyWebSecurityManager.class);

       /** SecurityManager instance. */
       private final SecurityManager sm;

       /**
        * Public constructor. Accepts a SecurityManager instance to avoid deadlocks.
        *
        * @param    sm
        *           SecurityManager instance.
        */
       LegacyWebSecurityManager(SecurityManager sm)
       {
          this.sm = sm;
       }

       /**
        * The map of currently active web requests, with their context data.  The key is the token resolved after
        * the initial authentication was performed.
        */
       private Map<String, WebRequestContext> webRequestContexts = new ConcurrentHashMap<>();

       /**
        * A thread used to cleanup the expired {@link #webRequestContexts}.  Only contexts which are not active
        * being used will be cleaned.
        */
       private Thread webServiceContextCleaner = null;

       /**
        * The work which does the authentication and authorization requires a server context.  As it can
        * originate either from the web request thread or from the web service worker thread, an
        * {@link AssociatedThread} will be started if any web services has enabled authentication, and this is
        * where the work will be performed.
        */
       private WebServiceLogin webServiceLogin = null;

       /**
        * Check if the given credentials (user account and {@link UserAccount#getWebServiceToken() token}) is
        * allowed to access FWD web services.  Authorization will be done later, via {@link #checkAuthorization}.
        *
        * @param    account
        *           The FWD user account.
        * @param    webServiceToken
        *           The web service token.
        *
        * @return   <code>true</code> if there is a FWD user account with that name and web service token.
        *
        * @throws   RestrictedUseException
        *           If this method is called from restricted context
        */
       public boolean authWebRequest(String account, String webServiceToken)
       throws RestrictedUseException
       {
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.main.BasicAuth.authenticate");

          if (!sm.isServer())
          {
             return false;
          }

          // safely query cached data
          SecurityCache sc = sm.getCache();

          Account acc = sc.getAccountById(account);
          if (acc == null || acc.getAccountType() != Account.ACC_USER)
          {
             return false;
          }

          if (!webServiceToken.equals(((UserAccount) acc).getWebServiceToken()))
          {
             return false;
          }

          // we allow tracking of these users.
          Object[] res = sm.allowAccount(null, acc.getSubjectId(), true);

          return res == null;
       }

       /**
        * Authenticate and create the FWD context
        *
        * @param    account
        *           The FWD user account.
        * @param    webServiceToken
        *           The web service token.
        * @param    timeout
        *           The timeout after which the FWD context will be {@link #destroyWebRequestContext destroyed}.
        *           If set to zero, the context will live until a logout operation is performed.
        *
        * @return   The authentication token which can be used for other calls.
        *
        * @throws   RestrictedUseException
        *           If this method is called from restricted context
        */
       public String createWebRequestContext(String account, String webServiceToken, int timeout)
       throws RestrictedUseException
       {
          SecurityUtil.checkCallerAbort("com.goldencode.p2j.main.BasicAuth.login");

          if (!sm.isServer())
          {
             return null;
          }

          // safely query cached data
          SecurityCache sc = sm.getCache();

          Account acc = sc.getAccountById(account);
          if (acc == null || acc.getAccountType() != Account.ACC_USER)
          {
             return null;
          }

          if (!webServiceToken.equals(((UserAccount) acc).getWebServiceToken()))
          {
             return null;
          }

          RestrictedUseException[] errors = new RestrictedUseException[1];
          CountDownLatch wait = new CountDownLatch(1);
          String[] token = new String[1];

          Runnable login = () ->
          {
             try
             {
                ContextSwitcher switcher = new ContextSwitcher();
                token[0] = UUID.randomUUID().toString();
                WebRequestContext context = new WebRequestContext(switcher, token[0], account, timeout);

                // we allow tracking of these users.
                Object[] res;
                try
                {
                   res = sm.postAuthenticateWorker(context, null, acc.getSubjectId(), true);
                }
                catch (RestrictedUseException e)
                {
                   token[0] = null;
                   errors[0] = e;
                   return;
                }

                int authResult = (int) res[0];
                if (authResult != AUTH_RESULT_SUCCESS)
                {
                   token[0] = null;
                   return;
                }

                SecurityContext newContext = (SecurityContext) res[3];

                if (newContext == null || sc != sm.getCache())
                {
                   LOG.finer("createWebRequestContext: newer security cache generation available; " +
                                "authentication aborted.");
                   token[0] = null;
                   return;
                }

                webRequestContexts.put(token[0], context);
             }
             finally
             {
                wait.countDown();
             }
          };

          // this needs to be executed on a server context thread
          webServiceLogin.addTask(login);

          try
          {
             wait.await();
          }
          catch (InterruptedException e)
          {
             // ignore
          }

          if (errors[0] != null)
          {
             throw errors[0];
          }

          return token[0];
       }

       /**
        * Start the {@link #webServiceContextCleaner} and {@link #webServiceLogin} threads, using the server
        * context.
        */
       public void startWebServiceContextThreads()
       {
          if (sm.isServer())
          {
             // create the thread for cleaning the web request contexts
             webServiceContextCleaner = new AssociatedThread(this::cleanExpiredWebRequestContexts);
             webServiceContextCleaner.setName("WebServiceContextCleaner");
             webServiceContextCleaner.setDaemon(true);
             webServiceContextCleaner.start();

             webServiceLogin = new WebServiceLogin();
             Thread t = new AssociatedThread(webServiceLogin);
             t.setName("WebServiceLogin");
             t.setDaemon(true);
             t.start();
          }
       }

       /**
        * Destroy the context for the web request identified by the specified token.
        *
        * @param    token
        *           The authentication token.
        *
        * @return   <code>true</code> if {@link LegacyWebSecurityManager#webRequestContexts} has this token and
        *           the FWD session was terminated.
        */
       public boolean destroyWebRequestContext(String token)
       {
          WebRequestContext context = webRequestContexts.remove(token);

          if (context == null)
          {
             return false;
          }

          try
          {
             sm.sessionSm.terminateSessionById(context);
          }
          catch (RestrictedUseException e)
          {
             LOG.log(LOG.isLoggable(Level.FINE) ? Level.FINE : Level.WARNING,
                     "Could not destroy context for token: " + token,
                     e);
             return false;
          }

          return true;
       }

       /**
        * Check if the specified token is associated with an active and not expired web request context.
        * <p>
        * If the web request is expired, it will be {@link #destroyWebRequestContext destroyed}.
        *
        * @param    token
        *           The authentication token.
        *
        * @return   <code>true</code> if the token exists and the web request is not expired.
        */
       public boolean hasWebRequestContext(String token)
       {
          WebRequestContext context = webRequestContexts.get(token);

          if (context != null)
          {
             if (context.isExpired())
             {
                destroyWebRequestContext(token);
                return false;
             }

             return true;
          }

          return false;
       }

       /**
        * Execute the specified task in the context associated with the web request identified via the specified
        * authentication token.
        *
        * @param    token
        *           The authentication token.
        * @param    task
        *           The task to be performed.
        */
       public void executeInContext(String token, Runnable task)
       {
          executeInContext(token, task, false);
       }

       /**
        * Execute the specified task in the context associated with the web request identified via the specified
        * authentication token.
        *
        * @param    token
        *           The authentication token.
        * @param    task
        *           The task to be performed.
        * @param    useInitial
        *           Flag indicating to create an initial security context.
        */
       public void executeInContext(String token, Runnable task, boolean useInitial)
       {
          WebRequestContext context = webRequestContexts.get(token);

          if (context == null)
          {
             return;
          }

          ContextSwitcher switcher = context.getContextSwitcher();
          try
          {
             switcher.setContext(context, useInitial);
             task.run();
          }
          finally
          {
             switcher.releaseContext(useInitial);
          }
       }

       /**
        * Execute the given task in the {@link LegacyWebSecurityManager#webServiceLogin}  thread, so that it
        * can check if the web request identified with the given authentication token is authorized for that
        * web request.
        *
        * @param    token
        *           The authentication token.
        * @param    task
        *           The authorization task.
        *
        * @return   <code>true</code> if the web request token is authorized.
        */
       public boolean checkAuthorization(String token, Supplier<Boolean> task)
       {
          WebRequestContext context = webRequestContexts.get(token);

          if (context == null)
          {
             return false;
          }

          CountDownLatch wait = new CountDownLatch(1);
          boolean[] res = new boolean[1];
          Runnable work = () ->
          {
             ContextSwitcher switcher = context.getContextSwitcher();
             try
             {
                switcher.setContext(context, false);
                res[0] = task.get();
             }
             finally
             {
                switcher.releaseContext(false);
                wait.countDown();
             }
          };

          webServiceLogin.addTask(work);

          try
          {
             wait.await();
          }
          catch (InterruptedException e)
          {
             // ignore
          }

          return res[0];
       }

       /**
        * Given an authentication related code, return a {@link ContextSwitcher} instance, if the
        * authentication was performed.
        * <p>
        * This switcher allows a thread to establish the context, if it knows a session ID.
        *
        * @param    auth
        *           The authentication code.
        *
        * @return   A context switcher instance.
        */
       public ContextSwitcher getContextSwitcher(Runnable auth)
       {
          auth.run();

          return sm.contextSm.hasContext() ? new ContextSwitcher() : null;
       }

       /**
        * The main work performed by {@link #webServiceContextCleaner}, it goes through the
        * {@link #webRequestContexts} every 10 seconds and cleans any contexts which are not in use and are
        * expired.
        */
       private void cleanExpiredWebRequestContexts()
       {
          Object lock = new Object();
          while (true)
          {
             try
             {
                synchronized (lock)
                {
                   // TODO: configure this?
                   lock.wait(10 * 1000);  // 10 seconds wait
                }
             }
             catch (InterruptedException e)
             {
                break;
             }

             List<String> destroy = new ArrayList<>();
             for (WebRequestContext req : webRequestContexts.values())
             {
                if (req.isExpired() && !AppServerManager.isWebServiceTokenInUse(req.getToken()))
                {
                   destroy.add(req.getToken());
                }
             }

             for (String token : destroy)
             {
                destroyWebRequestContext(token);
             }
          }
       }

       /**
        * A thread inheriting the FWD server context, used to perform authentication and authorization work for
        * web requests.
        */
       private static class WebServiceLogin
       implements Runnable
       {
          /** The task to perform. */
          private List<Runnable> tasks = new ArrayList<>();

          /**
           * Main work loop.
           */
          public void run()
          {
             LinkedList<Runnable> work = new LinkedList<>();

             while (true)
             {
                synchronized (this)
                {
                   if (tasks.isEmpty())
                   {
                      try
                      {
                         wait();
                      }
                      catch (InterruptedException e)
                      {
                         // ignore
                      }
                   }

                   work.addAll(tasks);
                   tasks.clear();
                }

                for (Runnable task : work)
                {
                   try
                   {
                      task.run();
                   }
                   catch (Throwable t)
                   {
                      // ignore
                      LOG.log(Level.SEVERE, "Error processing login task", t);
                   }
                }

                work.clear();
             }
          };

          /**
           * Add a task to the {@link #tasks queue}.
           *
           * @param    task
           *           The task to perform.
           */
          synchronized void addTask(Runnable task)
          {
             tasks.add(task);
             this.notify();
          }
       }
    }