MetadataSecurityOps.java

/*
** Module   : MetadataSecurityOps.java
** Abstract : Progress 4GL compatible security interface
**
** Copyright (c) 2013-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------Description---------------------------------
** 001 OM  20130913 First version, declaring the basic four methods: get/set with and without the
**                  logical database name.
** 002 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 003 OM  20180219 Fixed getUserIdFromDB() for databases aliases.
** 004 OM  20180305 Dropped [defaultDatabase] notion.
** 005 CA  20181128 Allow assigning a USERID for a specified database without authentication.
**                  Used by cases when SET-DB-CLIENT provides a SSO CLIENT-PRINCIPAL.
** 006 IAS 20191017 Added "DOMAIN-NAME" metadata field definition
** 007 IAS 20220713 Added 'lock' argument to the setUserIdDirect method
**     ECF 20220526 Renamed _User metadata table fields to include leading underscore.
**     CA  20230607 The leading underscore for _User metadata table fields depends on the minimal conversion
**                  mode, so rely on this namespace attribute when resolving the field names.
*/

/*
** 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 com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.persist.*;

/**
 * Implementation of user security functions of P4GL using the _User metadata from database.
 *
 * From my tests, Progress does not use locks when executing <code>setuserid</code> and
 * <code>userid</code> functions. If a user is exclusive-locking the _user table, another one
 * can get/set userid for the same database. However, attempting to execute a FIND query on the
 * _user meta-table it will be put on hold until the first user releases the lock.
 *
 * TODO:
 * Err 709: "-P and -U startup parameters require _User file records."
 */
public class MetadataSecurityOps
implements CustomSecurityOps
{
   /**
    * Constant used for accessing the converted _User meta-table.
    */
   public static final String META_TABLE_USER =  "meta_user";
   
   /** The user ID field name, per ldb. */
   private static Map<String, String> metaFieldUserId = new ConcurrentHashMap<>();
   
   /** The domain field name, per ldb. */
   private static Map<String, String> metaFieldDomain = new ConcurrentHashMap<>();
   
   /** The password field name, per ldb. */
   private static Map<String, String> metaFieldPassword = new ConcurrentHashMap<>();
   
   /**
    * Compute the name of the user ID field, depending if the conversion for this ldb is minimal or not.
    * 
    * @return   The field name.
    */
   public static String getMetaFieldUserid(String ldbName)
   {
      // TODO:  this code is to be removed when we implement all metadata tables to use the underscore prefix 
      // instead of the meta_ prefix, and the meta fields will use the underscore always.
      String fieldName = metaFieldUserId.computeIfAbsent(ldbName.toLowerCase(), (k) ->
      {
         int cvt = MatchPhraseConstants.SQL_MODE_DEFAULT;
         try
         {
            cvt = Configuration.getSchemaConfig().getSqlConversion(ldbName);
         }
         catch (Exception e)
         {
            // ignore
         }
         
         return cvt == MatchPhraseConstants.SQL_MODE_DEFAULT ? "userid" : "_userid";
      });
      
      return fieldName;
   }

   /**
    * Compute the name of the domain field, depending if the conversion for this ldb is minimal or not.
    * 
    * @return   The field name.
    */
   public static String getMetaFieldDomain(String ldbName)
   {
      // TODO:  this code is to be removed when we implement all metadata tables to use the underscore prefix 
      // instead of the meta_ prefix, and the meta fields will use the underscore always.
      String fieldName = metaFieldDomain.computeIfAbsent(ldbName.toLowerCase(), (k) ->
      {
         int cvt = MatchPhraseConstants.SQL_MODE_DEFAULT;
         try
         {
            cvt = Configuration.getSchemaConfig().getSqlConversion(ldbName);
         }
         catch (Exception e)
         {
            // ignore
         }
         
         return cvt == MatchPhraseConstants.SQL_MODE_DEFAULT ? "domain_name" : "_domain_name";
      });
      
      return fieldName;
   }

   /**
    * Compute the name of the password field, depending if the conversion for this ldb is minimal or not.
    * 
    * @return   The field name.
    */
   public static String getMetaFieldPassword(String ldbName)
   {
      // TODO:  this code is to be removed when we implement all metadata tables to use the underscore prefix 
      // instead of the meta_ prefix, and the meta fields will use the underscore always.
      String fieldName = metaFieldPassword.computeIfAbsent(ldbName.toLowerCase(), (k) ->
      {
         int cvt = MatchPhraseConstants.SQL_MODE_DEFAULT;
         try
         {
            cvt = Configuration.getSchemaConfig().getSqlConversion(ldbName);
         }
         catch (Exception e)
         {
            // ignore
         }

         return cvt == MatchPhraseConstants.SQL_MODE_DEFAULT ? "password" : "_password";
      });
      
      return fieldName;
   }

   /**
    * Returns the userid associated with the current connected database. If no database is
    * connected or there are at least two databases connected empty string is returned.
    *
    * @return   The current user context's userid or the empty string in the case of any problem.
    */
   @Override
   public String getUserId()
   {
      // this 4GL functions tends (?) to return the user id of the "default" database
      // the "default" database is the one connected at compile time
      return getUserIdFromDB(ConnectionManager.getDefaultDatabase());
   }
   
   /**
    * Returns the userid associated with the current user of the given logical database.
    *
    * @param    dbname
    *           Logical database name.
    *
    * @return  The current user context's userid for specified database. If database is not known
    *          or not connected, the empty string is returned. If an {@code unknown} parameter is
    *          provided the method returns {@code unknown} value. 
    */
   @Override
   public String getUserIdFromDB(String dbname)
   {
      // null/empty validation
      if (dbname == null)
      {
         return null; // will be wrapped as [unknown] value 
      }
      
      // TODO: if the database is known but not connected, display/throw error:
      // 1072: "SETUSERID of database <db-name> requires that database to be connected".
      
      // if the database name is not known, blank user is returned:
      character ldbName = ConnectionManager.ldbName(dbname);
      if (ldbName.isUnknown())
      {
         // the 'dbname' is not a valid connected database logical name nor an alias
         // at runtime, we get an empty string (not unknown, not error)
         return SecurityOps.BLANK_USER;
      }
      
      // use the cached user id from ConnectionManager
      return ConnectionManager.getCurrentUserid(ldbName.toStringMessage());
   }
   
   /**
    * Authenticates the user for specified DB connection. Checks if the user login account match
    * the corresponding record of the _User table of the database. If there are multiple database
    * connections or none is connected, an error is issued.
    *
    * @param    userid
    *           The name of the user to set as UserID.
    * @param    password
    *           The users password.
    *
    * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
    */
   @Override
   public boolean setUserId(String userid, String password)
   {
      return setUserId(userid, password, ConnectionManager.getDefaultDatabase());
   }
   
   /**
    * Authenticates the user for specified DB connection. Checks if the user login account match
    * the corresponding record of the _User table of the database.  If the specified database
    * is not connected, an error is issued.
    *
    * The user/password comparing operation is done as 4GL:
    * <ul>
    *    <li><strong>user</strong>: is case-insensitive, trailing spaces are ignored 
    *             (in upper(rtrim()) mode)
    *    <li><strong>password</strong>: must be an exact match: after applying <code>ENCODE</code>
    *             to password string it is compared to existing record from database in case 
    *             sensitive, taking into consideration trailing spaces
    * </ul>
    *
    * @param    userid
    *           The name of the user to set as UserID.
    * @param    password
    *           The users password.
    * @param    dbname
    *           Logical database name.
    *
    * @return   The <code>true</code> in case of valid user match <code>false</code> otherwise.
    */
   @Override
   public boolean setUserId(String userid, String password, String dbname)
   {
      if (ConnectionManager.authenticate(userid, password, dbname))
      {
         ConnectionManager.setAuthenticatedUserid(dbname, userid, true, false);
         return true;
      }
      
      // authentication failed
      return false;
   }
   
   /**
    * Sets the specified user ID directly, without authentication, for the given database.
    * 
    * @param    userid
    *           The name of the user to set as UserID.
    * @param    dbname
    *           Logical database name.
    * @param    lock
    *           Flag  indicating that the database should be locked
    */
   @Override
   public void setUserIdDirect(String userid, String dbname, boolean lock)
   {
      ConnectionManager.setAuthenticatedUserid(dbname, userid, lock, false);
   }
   
   /**
    * Check the access level for a database. This will query the _User table and return the value.
    * 
    * @param   ldbname
    *          The logical database name to query. 
    * 
    * @return  The access level to specified database. 
    */
   @Override
   public int getAuthLevel(String ldbname)
   {
      return ConnectionManager.getAuthLevel(ldbname);
   }
   
   /**
    * Checks if the currently authenticated user has access to a database.
    * <p>
    * If the database does not have any users defined in _user meta table the authentication is
    * not enforced so any user can access it. Otherwise, the user must have entered a correct
    * combination of userid / password to get access to respective database.
    *
    * @param   ldbName
    *          The logical name of the database.
    *
    * @return  <code>true</code> if access to requested database is granted
    */
   @Override
   public boolean hasAccessToDatabase(String ldbName)
   {
      if (getAuthLevel(ldbName) != SecurityOps.STRICT)
      {
         return true;
      }
      
      final String currentUserid = ConnectionManager.getCurrentUserid(ldbName);
      // returning true if currentUserid is not null and not empty (blank)
      return currentUserid != null && !SecurityOps.BLANK_USER.equals(currentUserid);
   }
}