SecurityPolicyManager.java

/*
** Module   : SecurityPolicyManager.java
** Abstract : Provides SECURITY-POLICY attributes and methods support
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 IAS 20190502 Created initial version.
** 002 IAS 20190515 Added runtime support for attributes and cryptography.
** 003 IAS 20190619 Added runtime support for GET-CLIENT, SET-CLIENT, REGISTER-DOMAIN, 
**                  LOCK-REGISTRATION, LOAD-DOMAINS.
** 004 IAS 20190703 Changed support for LOAD-DOMAINS, SET-CLIENT, SET-DB-CLIENT 
**                  for non-default domains
** 005 IAS 20191015 Rework SET-CLIENT/SET-DB-CLIENT
** 006 CA  20191119 Disabled the 16388 error in setClient, as it fails if the DBs are already
**                  locked with the same client-principal.
** 007 IAS 20200115 Added domain access code lookup by domain name
** 008 OM  20201203 Fixed handling of READ/ONLY attributes.
** 009 IAS 20220415 Misc. fixes for error reporting.
** 010 CA  20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 011 OM  20241015 Fixed database compare in multi-tenant case. Import cleanup.
** 012 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
**                  based on [sharedDb] parameter.
** 013 OM  20250227 Local optimizations: avoided multiple context lookups, etc.
*/

/*
** 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.util;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import org.apache.commons.lang3.StringUtils; // Need explicit import to avoid ambiguity
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.ClientPrincipal.*;
import com.goldencode.p2j.util.ClientPrincipalResource.*;
import com.goldencode.p2j.util.CryptoUtils.*;
import com.goldencode.p2j.util.SecurityOps.*;
import com.goldencode.proxy.*;

/**
 * Provides static methods implementing support for the SECURITY-POLICY system
 * handle attributes and methods See {@link CommonSecurityPolicy}
 * 
 */
public class SecurityPolicyManager
extends SystemHandleBase
{
   /**
    * Supported symmetric ciphers' list as String.
    */
   private static final String CIPHER_NAMES = String.join(",", CryptoUtils.CIPHERS);

   /**
    * Supported PBE hash algorithms.
    */
   private static final Set<String> DIGESTS = Collections.unmodifiableSet(CryptoUtils.DIGESTS.keySet());

   /**
    * Context local proxy that allows SECURITY-POLICY attributes to be pushed from
    * server to the client.
    */
   private static final ContextLocal<WorkArea> work = new ContextLocal<WorkArea>()
   {
      /**
       * Initializes the work area, the first time it is requested within a new
       * context.
       * <p>
       * This will obtain a unique instance of <code>SessionExports</code> depending
       * on which side (server or client) it is called the returned value can be a
       * local or a network proxy instance.
       *
       * @return The newly instantiated work area.
       */
      @Override
      protected WorkArea initialValue()
      {
         return new WorkArea();
      }
   };

   /**
    * Get a the instance for the SECURITY-POLICY system handle. Is obtained using a
    * call to the {@link StaticProxy#obtain(Class, Class[])}, using the
    * {@link CommonSecurityPolicy} interface and its methods implemented by these
    * classes: {@link SecurityPolicyManager}.
    * 
    * @return See above.
    */
   public static handle asHandle()
   {
      WorkArea wa = work.get();

      Class<?>[] classes = { SecurityPolicyManager.class };
      CommonSecurityPolicy proxy = StaticProxy.obtain(CommonSecurityPolicy.class, classes);

      if (wa.id == null)
      {
         wa.id = handle.resourceId(proxy);
      }
      return new handle(proxy);
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-SUPPORT attribute getter. Returns
    * a comma-separated list of supported cryptographic algorithm names to use in
    * encrypting and decrypting data. Each algorithm name is a concatenation of
    * three character expressions that identify an algorithm, mode, and key size.
    * 
    * @return A comma-separated list of supported cryptographic algorithm names.
    */
   public static character getSymmetricSupport()
   {
      return new character(CIPHER_NAMES);
   }

   /**
    * Provides the decoded value of the
    * SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM attribute. For internal use
    * 
    * @return the decoded value of the
    *         SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM attribute.
    */
   static CipherParams getSymmetricCiperParams()
   {
      return new CipherParams(work.get().symmetricEncryptionAlgorithm);
   }

   /**
    * Provides the decoded value of the symmetric encryption algorithm For internal
    * use.
    * 
    * @param spec The algorithm spec string. If is is null or empty the value of
    *             the SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM attribute is
    *             decoded
    * 
    * @return the decoded value of the symmetric encryption algorithm.
    * 
    * @throws java.lang.IllegalArgumentException if the invalid parameter was
    *       provided
    */
   static CipherParams getSymmetricCipherParams(String spec)
   {
      return new CipherParams(
            StringUtils.isEmpty(spec) ? work.get().symmetricEncryptionAlgorithm : spec);
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM attribute
    * getter. This is write-only attribute, so the getter always return error
    * 
    * @return error (4052).
    */
   public static character getSymmetricEncryptionAlgorithm()
   {
      return new character(work.get().symmetricEncryptionAlgorithm);
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM attribute
    * setter. Sets the name of the default cryptographic algorithm to use with the
    * ENCRYPT and DECRYPT functions.
    * 
    * @param aname The name of the default cryptographic algorithm.
    */
   public static void setSymmetricEncryptionAlgorithm(character aname)
   {
      if (aname.isUnknown() || !CryptoUtils.CIPHERS.contains(StringUtils.upperCase(aname.value)))
      {
         invalidValue(12223, "SYMMETRIC-ENCRYPTION-ALGORITHM");
         return;
      }
      work.get().symmetricEncryptionAlgorithm = aname.value;
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-ENCRYPTION-KEY attribute getter.
    * This is write-only attribute, so the getter always return error
    * 
    * @return error (4052).
    */
   public static raw getSymmetricEncryptionKey()
   {
      ErrorManager.displayError(4052,
            "SYMMETRIC-ENCRYPTION-KEY is not a queryable attribute for PSEIDO-WIDGET");
      return raw.instantiateUnknownRaw();
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM attribute
    * setter. Sets the default encryption key (a binary value) to use with the
    * ENCRYPT and DECRYPT functions. The default value is the Unknown value (?).
    * 
    * @param key The default encryption key.
    */
   public static void setSymmetricEncryptionKey(raw key)
   {
      work.get().encryptionKey = key.isUnknown() ? null : key.getByteArray();
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-ENCRYPTION-IV attribute getter.
    * Returns the default initialization vector value to use with the encryption
    * key in the ENCRYPT and DECRYPT functions. The default value is the Unknown
    * value (?), which indicates that no initialization vector value is used.
    * 
    * @return The default initialization vector value.
    */
   public static raw getSymmetricEncryptionIV()
   {
      byte[] iv = work.get().encryptionIV;
      // Contrary to the 4GL doc the default value of this attribute is empty RAW, not UNKNOWN
      // And if the UNKNOWN value is assigned the empty RAW is returned as well
      return iv == null ? new raw(new byte[0]) : new raw(iv.clone());
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-ENCRYPTION-IV attribute setter.
    * Sets the default initialization vector value to use with the encryption key
    * in the ENCRYPT and DECRYPT functions. The default value is the Unknown value
    * (?), which indicates that no initialization vector value is used.
    * 
    * @param iv The default initialization vector value.
    */
   public static void setSymmetricEncryptionIV(raw iv)
   {
      work.get().encryptionIV = iv.isUnknown() ? null : iv.getByteArray();
   }

   /**
    * Implementation of ENCRYPTION-SALT attribute getter. Returns the default salt
    * value (a random series of bytes) to use with the GENERATE-PBE-KEY function.
    * The default value is the Unknown value (?), which indicates that no salt
    * value is used to generate the password-based encryption key.
    * 
    * @return The default salt value.
    */
   public static raw getEncryptionSalt()
   {
      byte[] salt = work.get().encryptionSalt;
      return salt == null ? raw.instantiateUnknownRaw() : new raw(salt.clone());
   }

   /**
    * Implementation of SECURITY-POLICY:ENCRYPTION-SALT attribute setter. Sets the
    * default salt value (a random series of bytes) to use with the
    * GENERATE-PBE-KEY function. The default value is the Unknown value (?), which
    * indicates that no salt value is used to generate the password-based
    * encryption key.
    * 
    * @param salt The default salt value.
    */
   public static void setEncryptionSalt(raw salt)
   {
      work.get().encryptionSalt = salt.isUnknown() ? null : salt.getByteArray();
   }

   /**
    * Implementation of PBE-KEY-ROUNDS attribute getter. Returns the number of hash
    * algorithm iterations to perform in the GENERATE-PBE-KEY function to generate
    * a password-based encryption key. The value must be a positive integer. The
    * default value is 1000.
    * 
    * @return The number of hash algorithm iterations.
    */
   public static integer getPbeKeyRounds()
   {
      return new integer(work.get().pkeKeyRounds);
   }

   /**
    * Implementation of SECURITY-POLICY:PBE-KEY-ROUNDS attribute setter. Sets the
    * number of hash algorithm iterations to perform in the GENERATE-PBE-KEY
    * function to generate a password-based encryption key. The value must be a
    * positive integer. The default value is 1000.
    * 
    * @param rounds The number of hash algorithm iterations.
    */
   public static void setPbeKeyRounds(integer rounds)
   {
      if (rounds.isUnknown() || rounds.intValue() <= 0)
      {
         invalidValue(12222, "PBE-KEY-ROUNDS");
         return;
      }
      work.get().pkeKeyRounds = rounds.intValue();
   }

   /**
    * Implementation of PBE-HASH-ALGORITHM attribute getter. Returns the number of
    * hash algorithm iterations to perform in the GENERATE-PBE-KEY function to
    * generate a password-based encryption key. The value must be a positive
    * integer. The default value is 1000.
    * 
    * @return The number of hash algorithm iterations.
    */
   public static character getPbeHashAlgorithm()
   {
      return new character(work.get().pbeHashAlgorithm);
   }

   /**
    * Implementation of SECURITY-POLICY:PBE-HASH-ALGORITHM attribute setter. Sets
    * the number of hash algorithm iterations to perform in the GENERATE-PBE-KEY
    * function to generate a password-based encryption key. The value must be a
    * positive integer. The default value is 1000.
    * 
    * @param aname The number of hash algorithm iterations.
    */
   public static void setPbeHashAlgorithm(character aname)
   {
      if (aname.isUnknown() || !DIGESTS.contains(StringUtils.upperCase(aname.value)))
      {
         invalidValue(12221, "SYMMETRIC-ENCRYPTION-ALGORITHM");
         return;
      }
      work.get().pbeHashAlgorithm = aname.value;
   }

   /**
    * Implementation of SECURITY-POLICY:GET-CLIENT method. Returns the handle to a
    * copy of the sealed client-principal object that represents the user identity
    * for the ABL session. If no identity has been established for the session
    * using the SECURITY-POLICY:SET-CLIENT( ) method, this method returns the
    * Unknown value (?).
    * 
    * @return The handle to a copy of the sealed client-principal object.
    */
   public static handle getClient()
   {
      return work.get().client;
   }

   /**
    * Get domain access code by domain name.
    *  
    * @param domainName domain name.
    * @param error <code>Consumer</code> for an error code.
    * 
    * @return optional domain code if found
    */
   public static Optional<String> getDomainAccessCode(String domainName, Consumer<DomainStateError> error)
   {
      DomainRegistry registry = work.get().domainRegistry;
      if (!registry.loadDomainsWasCalled && !registry.registerDomainWasCalled)
      {
         error.accept(DomainStateError.REGISTRY_IS_EMPTY);
         return Optional.empty();
      }
      if (!registry.isLocked())
      {
         error.accept(DomainStateError.REGISTRY_IS_NOT_LOCKED);
         return Optional.empty();
      }
      Domain domain = registry.get(domainName);
      if (domain == null)
      {
         error.accept(DomainStateError.DOMAIN_NOT_EXISTS);
         return Optional.empty();
      }
      if (!domain.isEnabled())
      {
         error.accept(DomainStateError.DOMAIN_DISABLED);
         return Optional.empty();
      }
      return Optional.of(domain.getAccessCode());
   }
   
   /**
    * Implementation of SECURITY-POLICY:SET-CLIENT method. Sets the identity, using
    * an unsealed or a sealed client-principal object, for the current ABL session,
    * and for all connected and unlocked OpenEdge database connections.
    * 
    * @param   h
    *          The handle to an unsealed or a sealed client-principal object.
    *
    * @return  {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical setClient(handle h)
   {
      if (h == null || h.isUnknown())
      {
         ErrorManager.recordOrShowError(
                  15918, 
                  "Required parameter for SET-CLIENT was passed the Unknown value",
                  true,
                  false,
                  false,
                  false
         );
         return logical.FALSE;
      }
      
      WorkArea wa = work.get();
      DomainRegistry registry = wa.domainRegistry;
      if (!registry.loadDomainsWasCalled && !registry.registerDomainWasCalled)
      {
         String msg = ConnectionManager.getConnectedDbCount() == 0 ?
                "session registry database not connected" :
                "the session registry is not loaded";
               
         ErrorManager.recordOrShowError(new int[] {15945}, 
               new String[] { "SECURITY-POLICY:SET-CLIENT failed because: " + msg },
               false, false, false, false, false);
         return logical.FALSE;
      }
      
      ClientPrincipalResource clientPrincipal = (ClientPrincipalResource) h.getResource();
      LoginState loginState = LoginState.valueOf(clientPrincipal.getLoginState().toStringMessage());
      Domain domain = registry.get(clientPrincipal.getDomainName().toStringMessage());
      switch (loginState)
      {
         case INITIAL:
            if (domain instanceof PersistentDomain)
            {
               if (!SecurityOps._USER_TABLE_AUTH_SYSTEM.equalsIgnoreCase(domain.getType().getName()))
               {
                  ClientPrincipal.authenticationFailed(16392, "Session", 
                                                       "Domain authentication action not supported ");
                  clientPrincipal.authenticationFailed("The Domain authentication action is not allowed");
                  return logical.FALSE;
               }
               return ((PersistentDomain)domain).validate(clientPrincipal);
            }
            
            if (registry.loadDomainsWasCalled && domain == null)
            {
               ClientPrincipal.authenticationFailed(16388, "Session", 
                                                    "Cannot find Domain configuration ");
               clientPrincipal.authenticationFailed("Cannot find the named PAM domain service");
               return logical.FALSE;
            }
            authenticationNotSupported();
            return logical.FALSE;
            
         case EXPIRED:
            if (registry.db == null)
            {
               authenticationNotSupported();
               return logical.FALSE;
            }
            
            if (clientPrincipal.isSealed())
            {
               ClientPrincipal.validationFailed(16376, "Session", "User authentication expired");
               return logical.FALSE;
            }
            
            // "princpal" - as in 4GL 
            ErrorManager.recordOrShowError(new int[] {15944}, 
                  new String[] {
                        "Failed Session client princpal authentication because: the " +
                        "Client-Principal is in an invalid state"
                  },
                  false, false, false, false, false);
            return logical.FALSE;
            
         case LOGIN:
            if (!registry.isLocked())
            {
               ClientPrincipal.validationFailed(16366, "Session", 
                     "Internal Authentication subsystem error [41]");
               return logical.FALSE;
            }
            if (domain == null)
            {
               ClientPrincipal.validationFailed(16389, "Session", "Nonexistent Domain");
               return logical.FALSE;
            }
            if (!clientPrincipal.validateDomainAccessCode(
                     "Session", domain.getAccessCode(), SealCallee.SET_CLIENT).booleanValue())
            {
               ClientPrincipal.validationFailed(16385, "Session", "The client-principal was corrupt");
               ErrorManager.recordOrShowError(new int[] { 13691 },
                        new String[] { String.format("Failed to set %s user id.", "Session") }, false,
                        false, false, false, false);
               return logical.FALSE;
            }
            
            ArrayList<String> unlockedDBs = ConnectionManager.getUnlocked();
            if (false && (domain instanceof RuntimeDomain) && !unlockedDBs.isEmpty())
            {
               // TODO: if all DBs are locked, then don't fail if at least one is already auth with
               // the same client-principal...
               ClientPrincipal.validationFailed(16388, ConnectionManager.pdbName(1).toStringMessage(), 
                     "Cannot find Domain configuration");
               return logical.FALSE;
            }
            
            // TODO: process loaded domain
            boolean ok = true;
            for (int i = 0; i < unlockedDBs.size(); i++)
            {
               ok &= SecurityOps.setDbClient(h, unlockedDBs.get(i), SealCallee.SET_CLIENT).booleanValue();
            }
            wa.client = h;
            return logical.of(ok);
            
         case LOGOUT:
            ClientPrincipal.validationFailed(16367, "Session", "Insufficient privileges");
            return logical.FALSE;
            
         case FAILED:
            ClientPrincipal.validationFailed(16369, "Session", "Failed account authentication");
            return logical.FALSE;
            
         default: // TODO: handle other states
            return logical.FALSE;
      }
   }

   /**
    * Implementation of SECURITY-POLICY:REGISTER-DOMAIN method. Registers a
    * security domain in the ABL session domain registry. The AVM uses this
    * registry to authenticate or validate (through a single sign-on (SSO)
    * operation) the session identity represented by a client-principal object, as
    * well as the connection identity for any OpenEdge database configured to use
    * the session (application) registry.
    * 
    * @param   domainName
    *          A character expression that specifies the name of this security domain.
    * @param   accessCode
    *          A character expression that specifies the secret value to use when authenticating or validating
    *          a client-principal object that represents a user identity in this domain.
    *
    * @return  {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical registerDomain(character domainName, character accessCode)
   {
      return registerDomain(domainName, accessCode, new character(), new character());
   }

   /**
    * Implementation of SECURITY-POLICY:REGISTER-DOMAIN method. Registers a
    * security domain in the ABL session domain registry. The AVM uses this
    * registry to authenticate or validate (through a single sign-on (SSO)
    * operation) the session identity represented by a client-principal object, as
    * well as the connection identity for any OpenEdge database configured to use
    * the session (application) registry.
    * 
    * @param   domainName
    *          A character expression that specifies the name of this security domain.
    * @param   accessCode
    *          A character expression that specifies the secret value to use when authenticating or validating
    *          a client-principal object that represents a user identity in this domain.
    * @param   domainDesc
    *          An optional character expression that specifies a description for this domain.
    *
    * @return  {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical registerDomain(character domainName, character accessCode, character domainDesc)
   {
      return registerDomain(domainName, accessCode, domainDesc, new character());
   }

   /**
    * Implementation of SECURITY-POLICY:REGISTER-DOMAIN method. Registers a
    * security domain in the ABL session domain registry. The AVM uses this
    * registry to authenticate or validate (through a single sign-on (SSO)
    * operation) the session identity represented by a client-principal object, as
    * well as the connection identity for any OpenEdge database configured to use
    * the session (application) registry.
    * 
    * @param   domainName
    *          A character expression that specifies the name of this security domain.
    * @param   accessCode
    *          A character expression that specifies the secret value to use when authenticating or validating
    *          a client-principal object that represents a user identity in this domain.
    * @param   domainDesc
    *          An optional character expression that specifies a description for this domain.
    * @param   domainType
    *          An optional character expression that specifies an application-defined authentication system
    *          for user authentication and single-sign-on (SSO) operations.
    *
    * @return  {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical registerDomain(character domainName,
                                        character accessCode,
                                        character domainDesc,
                                        character domainType)
   {
      DomainRegistry registry = work.get().domainRegistry;
      if (registry.isLocked())
      {
         ErrorManager.recordOrShowError(new int[] {14560}, 
               new String[] {
                  "SECURITY-POLICY:REGISTER-DOMAIN failed because the registry is already locked"
               },
               false, false, false, false, false);
         return logical.FALSE;
      }
      registry.registerDomainWasCalled = true;
      DomainType dt = SecurityOps.addDomainType(
            null, 
            domainType == null || domainType.isUnknown() ? null : domainType.toStringMessage(), 
            null, true
      );
      RuntimeDomain domain = new RuntimeDomain(
            domainName == null || domainName.isUnknown() ? "" : domainName.toStringMessage(),
            dt,
            domainDesc == null || domainDesc.isUnknown() ? null : domainDesc.toStringMessage(),
            accessCode == null || accessCode.isUnknown() ? null : accessCode.toStringMessage()
      );
      registry.add(domain);
      return logical.TRUE;
   }

   /**
    * Implementation of SECURITY-POLICY:LOCK-REGISTRATION method. Prevents the
    * registration of additional domains in the ABL session domain registry for the
    * remainder of an ABL session. You must call this method to use the domains you
    * have registered in the session domain registry using the REGISTER-DOMAIN( )
    * method.
    * 
    * @return  {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical lockRegistration()
   {
      DomainRegistry registry = work.get().domainRegistry;
      boolean locked = registry.lock();
      if (!locked)
      {
         ErrorManager.recordOrShowError(new int[] {14562}, 
               new String[] {
                  "SECURITY-POLICY:LOCK-REGISTRATION failed because registry is already locked"
               },
               false, false, false, false, false);
      }
      
      return logical.of(locked);
   }

   /**
    * Loads registered domains from the specified (and connected) OpenEdge RDBMS into the ABL 
    * session domain registry. 
    *
    * @param   num
    *          The sequence number of a connected database from which to load registered domains.
    *
    * @return  {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical loadDomains(integer num)
   {
      DomainRegistry registry = work.get().domainRegistry;
      if (registry.registerDomainWasCalled)
      {
         return loadDomainsFailed(14566, 
               "SECURITY-POLICY:REGISTER-DOMAIN has already been called");
      }
      registry.loadDomainsWasCalled = true;
      
      if (num == null || num.isUnknown())
      {
         return invalidLoadDomainsParam();
      }
      int n = num.intValue();
      int ndb = ConnectionManager.getConnectedDbCount();
      if (n < 1 || n > ndb)
      {
         return invalidLoadDomainsParam();
      }
      String dbname = ConnectionManager.pdbName(ndb).toStringMessage();
      Persistence persistence = ConnectionManager.getPersistence(dbname);
      return loadDomains(dbname, persistence);
   }

   /**
    * Loads registered domains from the specified (and connected) OpenEdge RDBMS into the ABL 
    * session domain registry. 
    *
    * @param   num
    *          The sequence number of a connected database from which to load registered domains
    *
    * @return {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical loadDomains(int num)
   {
      return loadDomains(new integer(num));
   }

   /**
    * Loads registered domains from the specified (and connected) OpenEdge RDBMS into the ABL 
    * session domain registry. 
    *
    * @param   name
    *          The logical name or alias of a connected database from which to load registered domains.
    *
    * @return  {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical loadDomains(character name)
   {
      DomainRegistry registry = work.get().domainRegistry;
      if (registry.registerDomainWasCalled)
      {
         return loadDomainsFailed(14566, 
               "SECURITY-POLICY:REGISTER-DOMAIN has already been called");
      }
      registry.loadDomainsWasCalled = true;
      if (name == null || name.isUnknown())
      {
         return invalidLoadDomainsParam();
      }
      String db = name.value;
      Persistence persistence = ConnectionManager.getPersistence(db);
      if (persistence == null)
      {
         return invalidLoadDomainsParam();
      }
      return loadDomains(db, persistence);
   }

   /**
    * Loads registered domains from the specified (and connected) OpenEdge RDBMS into the ABL 
    * session domain registry. 
    *
    * @param   name
    *          The logical name or alias of a connected database from which to load registered domains.
    *
    * @return {@code true} if operation succeeded or {@code false} otherwise.
    */
   public static logical loadDomains(String name)
   {
      return loadDomains(new character(name));
   }
   
   /**
    * API needed to implement read-only attribute assignment (a 4GL "feature").
    * 
    * @param    attribute
    *           The attribute's name.
    *           
    * @see      handle#readOnlyError(handle, String)
    */
   public static void readOnlyError(String attribute)
   {
      handle.readOnlyError(asHandle(), attribute);
   }
   
   /**
    * API needed to implement read-only attribute assignment (a 4GL "feature").
    * 
    * @param    attribute
    *           The attribute's name.
    * @param    expr
    *           The value which is attempted to be assigned to the read-only attribute. 
    *
    * @see      handle#readOnlyError(handle, String, Object)
    */
   public static void readOnlyError(String attribute, Object expr)
   {
      handle.readOnlyError(asHandle(), attribute);
   }
   
   /**
    * Get this resource's ID.
    * 
    * @return   The resource's ID.
    */
   public static Long id()
   {
      WorkArea wa = work.get();
      
      return wa.id;
   }

   /**
    * Implementation of SECURITY-POLICY:SYMMETRIC-ENCRYPTION-KEY attribute getter
    * for internal use.
    * 
    * @return the symmetric encryption key value
    */
   static raw getSymmetricEncryptionKeyInternal()
   {
      byte[] key = work.get().encryptionKey;
      return key == null ? raw.instantiateUnknownRaw() : new raw(key);
   }

   /**
    * Report "authentication not supported" error.
    */
   private static void authenticationNotSupported()
   {
      ErrorManager.recordOrShowError(new int[] {15945}, 
            new String[] {
               "SECURITY-POLICY:SET-CLIENT failed because: authentication not supported by" +
               " application defined registry",
            },
            false, false, false, false, false);
   }

   /**
    * Loads registered domains from the specified (and connected) OpenEdge RDBMS into the ABL 
    * session domain registry.
    *
    * @param   dbName
    *          The database name.
    * @param   persistence
    *          The {@link Persistence} instance for a  connected database from which to load registered
    *          domains.
    *        
    * @return   {@code true} if operation succeeded or {@code false} otherwise.
    */
   private static logical loadDomains(String dbName, Persistence persistence)
   {
      DomainRegistry registry = work.get().domainRegistry;
      Database defDb = persistence.getDatabase(Persistence.SHARED_CTX).getDefault();
      if (registry.db != null && !defDb.equals(registry.db))
      {
         return invalidLoadDomainsParam();
      }
      
      SecurityOps.refreshUsersAndDomains(dbName, persistence);
      registry.db = defDb;
      registry.dbn = dbName;
      return logical.TRUE;
   }

   /**
    * Report LOAD-DOMAINS failure and return <code>logical.FALSE</code>.
    * 
    * @param   code
    *          The error code.
    * @param   reason
    *          A short message explaining the cause of failure.
    * 
    * @return  Always {@code false} 
    */
   private static logical loadDomainsFailed(int code, String reason)
   {
      ErrorManager.recordOrShowError(new int[] { code },
            new String[] {
                  String.format("SECURITY-POLICY:LOAD-DOMAINS failed because %s", reason) },
            false, false, false, false, false);
      return logical.FALSE;
   }

   /**
    * Report invalid parameter for LOAD-DOMAINS and return <code>logical.FALSE</code>.
    * 
    * @return <code>false</code> 
    */
   private static logical invalidLoadDomainsParam()
   {
      ErrorManager.recordOrShowError(new int[] { 14564 },
            new String[] { "Invalid parameter for SECURITY-POLICY:LOAD-DOMAINS" }, false, false,
            false, false, false);
      return logical.FALSE;
   }

   /**
    * Report invalid attribute value error.
    * 
    * @param   code
    *          The error code.
    * @param   aname
    *          The attribute name.
    */
   private static void invalidValue(int code, String aname)
   {
      int[] nums = { code, 3131 };
      String[] texts = { String.format("Invalid %s value", aname),
            String.format("Unable to set attribute %s in widget  of type PSEUDO-WIDGET.", aname) };
      ErrorManager.recordOrThrowError(nums, texts, false, false, false);
   }

   /**
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {
      /** The resource's ID. */
      private Long id = null;

      /** SECURITY-POLICY:SYMMETRIC-ENCRYPTION-ALGORITHM attribute value. */
      private String symmetricEncryptionAlgorithm = "AES_CBC_128";

      /** SECURITY-POLICY:PBE-HASH-ALGORITHM attribute value. */
      private String pbeHashAlgorithm = "SHA-1";

      /** SECURITY-POLICY:PBE-KEY-ROUNDS attribute value. */
      private int pkeKeyRounds = 1000;

      /** SECURITY-POLICY:ENCRYPTION-SALT attribute value. */
      private byte[] encryptionSalt = null;

      /** SECURITY-POLICY:SYMMETRIC-ENCRYPTION-KEY attribute value. */
      private byte[] encryptionKey = null;

      /** SECURITY-POLICY:SYMMETRIC-ENCRYPTION-IV attribute value. */
      private byte[] encryptionIV = null;
      
      /** domain registry. */
      private final DomainRegistry domainRegistry = new DomainRegistry();
      
      /** client. */
      private handle client = new handle();
   }
   
   /** Domain loaded from the connected permanent database. */
   private static class PersistentDomain
   extends Domain
   {
      /** The database name. */
      private final String dbn;
      
      /**
       * Constructor.
       * 
       * @param   dbn
       *          The database name.
       * @param   d
       *          {@link Domain} instance.
       */
      public PersistentDomain(String dbn, Domain d)
      {
         super(d);
         this.dbn = dbn;
      }
      
      /** 
       * Validate CLINE-PRINCIPAL. 
       * 
       * @param clientPrincipal
       *        client principal to be validated.
       *        
       * @return <code>true</code> when successful.
       */
      public logical validate(ClientPrincipalResource clientPrincipal)
      {
         String password = getUserPwd(new QUserId(clientPrincipal));
         return clientPrincipal.validatePassword("Session", null, password, SealCallee.SET_CLIENT);
      }

      /**
       * Get user password.
       * 
       * @param quid
       *        qualified user name.
       *        
       * @return user password or <code> null</code> if no user found. 
       */
      public String getUserPwd(QUserId quid)
      {
         Optional<Map<QUserId, String>> users = SecurityOps.getUsers(dbn);
         return users.map(m -> m.get(quid)).orElse(null);
      }
   }
   
   /**
    * Errors for {@code getDomainAccessCode()} method.
    */
   public enum DomainStateError
   {
      /** Registry is empty */
      REGISTRY_IS_EMPTY(14541, "registry is empty"),
      
      /** Registry is not locked */
      REGISTRY_IS_NOT_LOCKED(14541, "registry is not locked"),
      
      /** Domain not found */
      DOMAIN_NOT_EXISTS(14541, "domain does not exist in the registry"),
      
      /** Domain disabled */
      DOMAIN_DISABLED(16391, " - Domian is disabled");
      
      /** The error code. */
      public final int code;
      
      /** The error message */
      public final String message;
      
      /**
       * The constructor for this immutable object.
       * 
       * @param   code
       *          The error code.
       * @param   message
       *          The error message.
       */
      DomainStateError(int code, String message)
      {
         this.code = code;
         this.message = message;
      }
   }
   
   /** Runtime security domain.*/
   public static class RuntimeDomain 
   extends Domain
   {
      /**
       * Create a new domain and initialize it with the given state.
       * 
       * @param    name
       *           The domain's name.
       * @param    type
       *           The domain's type.
       * @param    description
       *           The domain's description.
       * @param    accessCode
       *           The domain's access code.
       */
      public RuntimeDomain(String name, DomainType type, String description, String accessCode)
      {
         super(name, type, description, accessCode, null);
      }
   }
   
   /** Domain registry. */
   private static class DomainRegistry
   {
      /** Flag indicating that domain is locked. */
      private final AtomicBoolean locked = new AtomicBoolean(false);
      
      /** Flag indicating that LOAD-DOMAINS was called. */
      private boolean loadDomainsWasCalled = false;
      
      /** Flag indicating that REGISTER-DOMIAN was called. */
      private boolean registerDomainWasCalled = false;
      
      /** Registered domains by name. */ 
      private final Map<String, RuntimeDomain> domains = new ConcurrentHashMap<>();

      /** Database used in the LOAD-DOMAINS call. */
      private Database db;
      
      /** Name of the database used in the LOAD-DOMAINS call. */
      private String dbn;
      
      /**
       * Lock registry.
       * 
       * @return <code>false</code> if the registry is already locked 
       */
      private boolean lock()
      {
         return locked.compareAndSet(false, true);
      }
      
      /** 
       * Add domain to the registry.
       * 
       * @param domain
       *        domain to be added.
       */
      public void add(RuntimeDomain domain)
      {
         domains.put(domain.getName(), domain);
      }
      
      /**
       * Retrieve domain by name.
       * 
       * @param name
       *        domain name.
       *
       * @return registered domain with the specified name or <code>null</code> if not found.
       */
      public Domain get(String name)
      {
         Domain d = SecurityOps.findDomain(dbn, name);
         if (d != null)
         {
            return new PersistentDomain(dbn, d);
         }
         return domains.get(name);
      }
      
      /**
       * Check if registry is locked.
       * 
       * @return <code>true</code> if the registry is locked.
       */
      private boolean isLocked()
      {
         return locked.get() || loadDomainsWasCalled;
      }
   }
}