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