TemporaryAccountPool.java

/*
** Module   : TemporaryAccountPool.java
** Abstract : temporary account pool implementation.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- -------------------------------Description------------------------------------
** 001 MAG 20131212 Created initial version.
** 002 CA  20140206 Read the temporary client pool size and timeout here, as these are specific
**                  to this class.
** 003 MAG 20140217 Fixed shut down code.
** 004 IAS 20160403 Replaced ConcurrentLinkedQueue with LinkedBlockingQueue, removed unnecessary
**                  synchronization 
** 005 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 006 DDF 20230620 Replaced static initialization of values from the directory configuration with
**                  a method called at server bootstrap.
** 007 GBB 20250404 Fixed the order or reading configs and initializing.
*/
/*
** 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.util.concurrent.*;
import java.util.logging.*;

import com.goldencode.p2j.admin.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;

/**
 * A temporary account pool implementation.
 * On server start-up all temporary accounts which belongs to "temps" group are deleted.
 * New accounts are created and added to pool. The new accounts are created disabled.
 * Because the password is stored in directory as a hash string, the clear text password 
 * is kept in memory (pool) inside a <code>TemporaryAccount</code> structure.
 * When an item is requested from pool the item is removed form pool and the associated
 * account is enabled before returned to caller.
 * If the pool is empty the current thread will wait for an specified amount of time to
 * get an item returned to pool by other threads.
 * When an item is returned to pool first the associated account is disabled than the item
 * is enqueued at the tail of the pool and other threads waiting for an item are notified.
 * In order to manipulate user accounts special permissions are mandatory.
 * A dedicated worker thread is spawned on start-up and is used to manage user accounts at 
 * runtime. This daemon thread is running on server security context having admin permissions.
 *  
 * @author mag
 *
 */
public class TemporaryAccountPool
{   
   /** Logger. */
   private static final CentralLogger LOG = CentralLogger.get(TemporaryAccountWorker.class.getName());
      
   /** Temporary accounts group name */
   private static final String TEMP_GROUP = "temps";
   
   /** Random generator length */
   public static final int GEN_LENGTH = 16; 

   /** Random generator digits */
   public static final int GEN_DIGITS = 8; 
   
   /** Random generator symbols */   
   public static final int GEN_SYMBOLS = 4; 
   
   /** FIFO Pool */
   private static final BlockingQueue<TemporaryAccount> pool = 
            new LinkedBlockingQueue<>();
   
   /** Pool Manager instance */
   private static volatile TemporaryAccountPool poolManager;
      
   /** Task used to change the user account status */
   private static TemporaryAccountWorker poolTask;

   /** poolTask guard */
   private static final Object taskLock = new Object();
   
   /** A daemon worker thread having a security context which allow to change user account. */
   private static AssociatedThread worker;

   /** Timeout in milliseconds. */
   private static int timeout = 30000;
   
   /** Pool size */
   private static int poolSize = 8;
   
   /**
    * Initialize pool. This happens on server start-up.
    * All temporary accounts are deleted a new accounts are created.
    */
   private TemporaryAccountPool()
   {
      // Pool size
      poolSize = ConfigItem.TEMP_CLIENT_POOL_SIZE.read(poolSize, Utils.DirScope.SERVER);

      // Pool timeout
      timeout = ConfigItem.TEMP_CLIENT_TIMEOUT.read(timeout, Utils.DirScope.SERVER);
      
      AdminServerImpl.setTargetLive(true);
      // Create TEMP_GROUP group if not exists
      createGroupIfNotExists();
      // Delete temporary accounts
      deleteTemporaryAccounts();
      // Create new temporary accounts
      createTemporaryAccounts();
      // Test if pool has been created
      if (pool.isEmpty())
      {
         String message = "Admin privileges are mandatory to create temporary account pool.";
         LOG.logp(Level.WARNING,
                  "TemporaryAccountPool",
                  "static initializer",
                  message);
      }
      else
      {
         // Refresh target
         AdminServerImpl.targetRefresh();
      }
      // Start worker thread
      startUp();
   }
   
   /**
    * Create new temporary accounts
    */
   private static void createTemporaryAccounts()
   {
      for (int i = 0;i < poolSize;i++)
      {
         TemporaryAccount account = createUser(false);
         if (account != null)
         {
            pool.add(account);
         }
      }      
   }
   
   /**
    * Delete all temporary accounts. 
    * Temporary accounts belong to TEMP_GROUP group.
    */
   private static void deleteTemporaryAccounts()
   {
      TaggedName[] users = AdminServerImpl.listGroupUsers(TEMP_GROUP);
      if (users != null)
      {
         for (TaggedName name : users)
         {
            UserDef user = AdminServerImpl.getUser(name.getName());
            if (user != null)
            {
               AdminServerImpl.deleteUser(user.subjectId);
            }
         }
      }
   }
   
   /**
    * Check if temporary group exists. If not create group.
    */
   private static void createGroupIfNotExists()
   {
      GroupDef groupDef = AdminServerImpl.getGroup(TEMP_GROUP);
      if (groupDef == null)
      {
         groupDef = new GroupDef();
         groupDef.subjectId = TEMP_GROUP;
         groupDef.description = "all temps";  
         AdminServerImpl.addGroup(groupDef);
      }
   }
     
   /**
    * Create a temporary user.
    * 
    * @param   enabled
    *          Initial account status.
    *          
    * @return  Created user account.
    */
   static TemporaryAccount createUser(boolean enabled)
   {
      TemporaryAccount account = null;
      // Generate random values for account
      String subject = RandomWordGenerator.create(GEN_LENGTH, GEN_DIGITS, 0);
      String password = RandomWordGenerator.create(GEN_LENGTH, GEN_DIGITS, GEN_SYMBOLS);
      // Fill account structure
      UserDef user = new UserDef();
      user.enabled = enabled;
      user.protect = true;
      user.person = "chui";
      user.subjectId = subject;
      user.password = HashPassword.hashPassword(password);
      user.mode = 1;
      user.groups = new String[] { TEMP_GROUP };
      // Create temporary account
      if (AdminServerImpl.addUser(user, null))
      {
         account = new TemporaryAccount(user.subjectId, password);
      }
      // Return created account.
      return account;
   }

   /**
    * Create and return the pool manager instance.
    * This is a singleton design pattern. 
    * 
    * @return  An instance of TemporaryAccountPool
    */
   public static TemporaryAccountPool getInstance()
   {
      return ManagerHolder.INSTANCE;
   }
   
   /**
    * Startup worker thread.
    */
   private static void startUp()
   {
      synchronized (taskLock) 
      {
         poolTask = new TemporaryAccountWorker();
         worker = new AssociatedThread(poolTask);
         worker.start();      
      }
   }
   
   /**
    * Shut down worker thread.
    */
   public static void shutDown()
   {
      synchronized (taskLock) 
      {
         poolTask.terminate();
         // wait for thread to die
         try
         {
            worker.join(5000);
         }
         catch (InterruptedException e)
         {
            LOG.logp(Level.WARNING,
                     "TemporaryAccountPool",
                     "shutDown",
                     "InterruptedException!",
                     e);         
         }
      }
   }
   
   /**
    * Get and remove an item from pool.
    * Enable the temporary account before return to caller. 
    * The item should be returned back to pool when no longer used.
    * On empty pool wait for an amount of time to get an item from pool.
    * If no items are available after waiting a null value is returned.
    * 
    * @return  The item at the head of pool or null if pool is empty and
    *          no item is available after waiting an amount of time. 
    */
   public TemporaryAccount poll()
   {
      long timeout = TemporaryAccountPool.timeout;
      TemporaryAccount item = null;
      try {
         item = TemporaryAccountPool.pool.poll(timeout, TimeUnit.MILLISECONDS);
         if (item != null)
         {
            // Enable account
            enableAccount(item, true);
         }
      } 
      catch (InterruptedException e)
      {
         LOG.logp(Level.WARNING,
                  "TemporaryAccountPool.poll()",
                  "",
                  "InterruptedException!",
                  e);         
      }
      return item;
   }

   /**
    * Disable temporary account and return the item to pool.
    * The item is enqueued at the tail of the pool. 
    * Notify other threads waiting for a pool item.
    * 
    * @param   item
    *          A TemporaryAccount item returned to pool.
    */
   public void tail(TemporaryAccount item)
   {
         // Change user account status
         enableAccount(item, false);
         TemporaryAccountPool.pool.add(item);
   }
   
   /**
    * Enable / disable temporary account.
    * 
    * @param   item
    *          Account pool item.
    * @param   enabled
    *          <code>true</code> enable account.
    *          <code>false</code> disable account.
    */
   private void enableAccount(TemporaryAccount item, boolean enabled)
   {
      // Change user account status
      Runnable core = new UpdateAccountTask(item, enabled);
      synchronized (taskLock) 
      {
         TemporaryAccountPool.poolTask.execute(core);               
      }
   }
   
   /**
    * Helper clasee for the lazt initialization of the TemporaryAccountPool instance
    */
   private static class ManagerHolder
   {
      /** TemporaryAccountPool instance will be initialized on the first access */
      public static final TemporaryAccountPool INSTANCE = new TemporaryAccountPool();
   }
}