TenantManager.java
/*
** Module : TenantManager.java
** Abstract : Class that handles tenants
**
** Copyright (c) 2024-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
** 001 RAA 20240423 Created initial version.
** RAA 20240424 Added javadoc for getTenant.
** RAA 20240426 Fixed order of parameters in changeTenantStatus and string handling in updateTenant.
** RAA 20240513 Made updateTenant and deleteTenant use PreparedStatements.
** RAA 20240517 Moved private methods towards the end of the class. Added clearParameters call in
** updateTenantImpl.
** RAA 20240604 Changed return type of addTenant and changeTenantStatus. Changed to a Map
** parameter in updateTenant function.
** RAA 20240607 Added synchronization blocks.
** RAA 20240610 Added listTenants function.
** RAA 20240611 Added synchronization for initialize. Added init field for checking if initialization
** has been done.
** RAA 20240612 Moved assignment of the init flag at the end of initialize method.
** RAA 20240613 Added calls for removing the stored database configuration when a tenant is changed.
** RAA 20240627 Re-factored multi-tenancy to allow multiple database instances per tenant.
** RAA 20240627 Fixed ending bracket in CREATE_DATABASES_TABLE SQL.
** RAA 20240627 Added tenant checks in addTenant, addDatabaseToTenant and changeTenantStatus.
** RAA 20240702 Replaced DBCredentials occurrences with TenantDatabaseDescriptor.
** RAA 20240710 Added landlordDatabase field. Added getDatabase function.
** RAA 20240715 Clarified the type of database name occurrences.
** RAA 20240717 Added support for the two tenant authentication tables. Adjusted initialization process.
** RAA 20240719 Added SQLs for creating the two tenant authentication tables.
** RAA 20240723 When deleting a tenant or one of its database instances, remove the LockTableUpdater
** and the Settings for that/those instance(s).
** RAA 20240726 Added logical database name logic. Simplified parameters of addDatabaseToTenant. Added
** TenantDatabaseDescriptorKey logic to the dbInstances map.
** RAA 20240807 Removed TenantDatabaseDescriptorKey occurrences. Added default tenant support.
** RAA 20240808 Authentication tables are no longer stored in the landlord database.
** 002 OM 20240815 The method getDatabase() returns a Database object of same type as the argument.
** OM 20240909 Concurrent multitenancy functionality implementation. Basic tenant metadata support.
** 003 OM 20241001 Added tenant id support. Incremental update.
** RAA 20241011 Added tenant description logic.
** 004 OM 20241022 Fixed database connection management.
** 005 RAA 20241106 Added tenant info field.
** RAA 20241108 Adjusted the tenant_name and tenant_info column sizes from 255 to 32767.
** 006 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 007 OM 20241204 The [sec_authentication_domain] and [sec_authentication_system] meta tables are accessed
** from default persistent database, not from [landlord] database.
** 008 OM 20250116 Added support for additional dialects for landlord database.
** 009 OM 20250126 Synchronized landlord database with _Tenant meta table.
** 20250129 Implemented management methods for domain administration.
** 010 OM 20250303 The tenant_name is fetched with the other columns of a domain.
** 011 OM 20250410 Added more invalidations for SecurityOps.
** 012 OM 20250228 Encrypted the password in [tenant_databases] using a strong symmetric algorithm.
** 013 OM 20250331 Added c3p0 connection parameters.
** 20250513 Made password encryption methods public to be accessed from outside.
** 014 OM 20250430 Improve landlord database connection resiliency: attempts to reconnect on connection lost.
** 20250501 Complex operations on SQL database are performed in transactions.
*/
/*
** 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.persist.orm;
import java.beans.*;
import java.nio.charset.*;
import java.security.*;
import java.sql.*;
import java.sql.Statement;
import java.util.*;
import java.util.function.*;
import java.util.logging.Level;
import com.fasterxml.jackson.databind.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.mchange.v2.c3p0.*;
import org.apache.commons.codec.*;
import org.apache.commons.codec.binary.*;
import org.apache.commons.codec.binary.Base64;
import javax.crypto.*;
import static com.goldencode.p2j.util.BlockManager.*;
/**
* Class responsible with handling tenants.
*/
public class TenantManager
{
/** The name of the default tenant. */
public static final String DEFAULT_TENANT_NAME = "Default";
/** The description of the default tenant. */
public static final String DEFAULT_TENANT_DESCRIPTION = "Default tenant";
/** The info of the default tenant. */
public static final String DEFAULT_TENANT_INFO = "";
/** The id of the default tenant. */
public static final int DEFAULT_TENANT_ID = 0;
/** The type of default tenant. */
public static final int TYPE_DEFAULT = 0;
/** The type of regular tenants. */
public static final int TYPE_REGULAR = 1;
/** The type of super tenants. */
public static final int TYPE_SUPER = 2;
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(TenantManager.class);
/** The lock used to synchronize calls. */
private static final Object lock = new Object();
/** The maximum number of (regular and super) tenants. 16Mi far exceeds the OE capabilities. */
private static final int MAX_TENANTS = 0xFF_FFFF;
/** Constant for identifying the {@code _sec-authentication-system} table. */
public static final String SEC_AUTHENTICATION_SYSTEM = "_sec-authentication-system";
/** Constant for identifying the {@code _sec-authentication-domain} table. */
public static final String SEC_AUTHENTICATION_DOMAIN = "_sec-authentication-domain";
/** The JSON property name for tenant name. */
public static final String JSON_TENANT_NAME = "tenantName";
/** The JSON property name for tenant description. */
public static final String JSON_TENANT_DESCRIPTION = "tenantDescription";
/** The JSON property name for tenant info. */
public static final String JSON_TENANT_INFO = "tenantInfo";
/** The JSON property name for tenant's physical database name. */
public static final String JSON_PHYSICAL_NAME = "physicalName";
/** The JSON property name for tenant's logical database name. */
public static final String JSON_LOGICAL_NAME = "logicalName";
/** The column name for tenant name in both landlord tables. */
private static final String COL_TENANT_NAME = "tenant_name";
/** The column name for tenant description in landlord's {@code master} table. */
private static final String COL_TENANT_DESC = "tenant_description";
/** The column name for tenant info in landlord's {@code master} table. */
private static final String COL_TENANT_INFO = "tenant_info";
/** The column name for tenant's physical database in landlord's {@code tenant_databases} table. */
private static final String COL_PHYSICAL_DB = "pdb_name";
/** The column name for tenant's logical database in landlord's {@code tenant_databases} table. */
private static final String COL_LOGICAL_DB = "ldb_name";
/** The column name for tenant's connection URL in landlord's {@code tenant_databases} table. */
private static final String COL_DB_URL = "url";
/** The column name for tenant's authentication username in landlord's {@code tenant_databases} table. */
private static final String COL_DB_USER = "username";
/** The column name for tenant's authentication password in landlord's {@code tenant_databases} table. */
private static final String COL_DB_PASS = "password";
/** The dialect specific object responsible for providing SQL statements for tenant management. */
private static Dialect.TenantSQL tenantSQL = null;
/** Map for retrieving a tenant based on its name. */
private static final Map<String, Tenant> tenants = new HashMap<>();
/** Map for retrieving a tenant based on its id. */
private static final Map<Integer, Tenant> tenantsById = new HashMap<>();
/** Map of update statements for the master table based on the column name given. */
private static final Map<String, String> updateStmts = new HashMap<>();
/** Flag for determining if this class has been initialized or not. */
private volatile static boolean init = false;
/** The listeners which will be notified in case of tenant changes. */
private static final ArrayList<TenantMetaChangeListener> metaListeners = new ArrayList<>();
/** The Symmetric cipher parameters for en/decryption of passwords. */
private static CryptoUtils.CipherParams cp = null;
/** The key for en/decryption of passwords. It is initialized from directory at first use. */
private static byte[] key = null;
/**
* The initialization vector for en/decryption of passwords. It is initialized from directory at first use.
*/
private static byte[] iv = null;
/** The data source for the pool of connections to landlord database.*/
private static final ComboPooledDataSource ds = new ComboPooledDataSource("landlord-ds");
/** Enable the support for multi-tenancy. This process also involves creating the default tenant. */
public static void enable()
{
synchronized (lock)
{
init = true;
addDefaultTenant();
}
}
/**
* Initialize multi-tenancy. This process involves:
* <ol>
* <li> Creating the required tables if they don't already exist.
* <li> Initializing the statements that can be of type prepared.
* <li> Logically connecting the tenants that are stored in the master table.
* </ol>
*
* @param landlordURL
* The URL of the landlord database.
* @param landlordUser
* The (optional) JDBC User for access to the landlord database.
* @param landlordPassword
* The (optional) JDBC Password for access to the landlord database.
*/
public static void initialize(String landlordURL, String landlordUser, String landlordPassword)
{
if (!init)
{
return;
}
synchronized (lock)
{
Dialect dialect = Dialect.getDialectForURL(landlordURL);
try
{
ds.setDriverClass(dialect.getDriverClassName());
}
catch (PropertyVetoException e)
{
throw new RuntimeException("Failed to set the SQL driver for landlord DB " + landlordURL, e);
}
ds.setJdbcUrl(landlordURL);
ds.setUser(landlordUser);
ds.setPassword(landlordPassword);
// ds.setProperties(props); // other properties, if needed in future
tenantSQL = dialect.getTenantSql();
initializeStatements(tenantSQL);
Connection conn = null;
boolean errorReported = false;
try
{
conn = getConnection(true);
createTables(conn);
// check if there are tenants stored in the landlord database so we can logically connect them
populateAllTenants(conn);
conn.commit();
}
catch (SQLException e)
{
if (conn != null)
{
try
{
conn.rollback();
}
catch (SQLException ex)
{
// will be logged/reported anyway
}
}
LOG.log(Level.WARNING, "Cannot connect to the landlord database due to " + e.getMessage(), e);
errorReported = true;
}
finally
{
if (conn != null)
{
try
{
conn.close();
}
catch (SQLException e)
{
if (!errorReported) // avoid duplicating error messages
{
LOG.log(Level.WARNING,
"Failed to close connection to landlord database due to " + e.getMessage(), e);
}
}
}
}
}
}
/**
* Get a list with all the tenants.
*
* @param errorSink
* A sink for errors encountered.
*
* @return A list with the registered tenants.
*/
public static ArrayList<Tenant> listTenants(List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return null;
}
return new ArrayList<>(tenants.values());
}
/**
* Insert a tenant into the master table.
*
* @param tenantName
* The name of the tenant.
* @param tenantDescription
* The description of the tenant.
* @param tenantInfo
* The info of the tenant.
* @param databases
* A list of the databases that will be used by the tenant.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation is performed successfully, {@code false} otherwise.
*/
public static boolean addTenant(String tenantName,
String tenantDescription,
String tenantInfo,
List<TenantDatabaseDescriptor> databases,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
synchronized (lock)
{
if (tenants.containsKey(tenantName))
{
errorSink.add("Tenant '" + tenantName + "' already exists in landlord database.");
return false;
}
int nextId = getNextId(false);
Tenant tenant = new Tenant(nextId, tenantName, tenantDescription, tenantInfo, databases);
Connection conn = null;
boolean errorReported = false;
try
{
conn = getConnection(true);
// add the tenant to landlord database
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getInsertTenant()))
{
stmt.setObject(1, tenant.tenantId);
stmt.setString(2, tenant.tenantName);
stmt.setString(3, tenant.tenantDescription);
stmt.setString(4, tenant.tenantInfo);
stmt.setBoolean(5, false);
stmt.execute();
}
for (int i = 0; i < databases.size(); i++)
{
TenantDatabaseDescriptor db = databases.get(i);
// add each declared physical database to the tenant
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getInsertTenantDatabases()))
{
stmt.setString(1, tenant.tenantName);
stmt.setString(2, db.getPdbName());
stmt.setString(3, db.getLdbName());
stmt.setString(4, db.getUrl());
stmt.setString(5, db.getUsername());
stmt.setString(6, encrypt(db.getPassword()));
setIntValueInStatement(stmt, 7, db.getC3p0MaxStmts());
setIntValueInStatement(stmt, 8, db.getC3p0MinPool());
setIntValueInStatement(stmt, 9, db.getC3p0MaxPool());
setIntValueInStatement(stmt, 10, db.getC3p0AcqInc());
setIntValueInStatement(stmt, 11, db.getC3p0MaxIdle());
stmt.execute();
}
for (TenantMetaChangeListener mt : metaListeners)
{
if (mt.getDatabase().equals(db.getLdbName()))
{
runInGlobalBlock(() -> mt.addNewTenant(tenant, errorSink));
}
}
}
tenants.putIfAbsent(tenant.tenantName, tenant);
tenantsById.put(Math.abs(nextId), tenant);
conn.commit();
return true;
}
catch (SQLException e)
{
if (conn != null)
{
try
{
conn.rollback();
}
catch (SQLException ex)
{
// will be logged/reported anyway
}
}
errorSink.add("Cannot insert tenant due to " + e.getMessage());
LOG.log(Level.WARNING, "Cannot insert tenant due to " + e.getMessage(), e);
errorReported = true;
}
finally
{
if (conn != null)
{
try
{
conn.close();
}
catch (SQLException e)
{
if (!errorReported) // avoid duplicating error messages
{
errorSink.add("Cannot insert tenant due to " + e.getMessage());
LOG.log(Level.WARNING, "Cannot insert tenant due to " + e.getMessage(), e);
}
}
}
}
}
return false;
}
/**
* Sets an integer value to a positional parameter in a prepared statement.
*
* @param prepStmt
* The statement. Must be already prepared.
* @param pos
* The position of the parameter.
* @param value
* The value. Can be null.
*
* @throws SQLException
* If an SQL issue occurs.
*/
private static void setIntValueInStatement(PreparedStatement prepStmt, int pos, Integer value)
throws SQLException
{
if (value == null)
{
prepStmt.setNull(pos, Types.INTEGER);
}
else
{
prepStmt.setInt(pos, value);
}
}
/**
* Function for adding a database instance to a tenant.
*
* @param tenantName
* The tenant for which to add the database instance.
* @param db
* The database information to be added.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation is performed successfully, {@code false} otherwise.
*/
public static boolean addDatabaseToTenant(String tenantName,
TenantDatabaseDescriptor db,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (tenantName.equals(DEFAULT_TENANT_NAME))
{
errorSink.add("Cannot add database instances to the default tenant.");
LOG.log(Level.WARNING, "Cannot add database instances to the default tenant.");
return false;
}
synchronized (lock)
{
Tenant tenant = tenants.get(tenantName);
if (tenant == null)
{
errorSink.add("Cannot add a database instance to a non-existing tenant.");
LOG.log(Level.WARNING, "Cannot add a database instance to a non-existing tenant.");
return false;
}
Connection conn = null;
boolean errorReported = false;
try
{
conn = getConnection(true);
String pdbName = db.getPdbName();
String ldbName = db.getLdbName();
if (tenant.getDatabaseCredentials(pdbName, true) != null &&
tenant.getDatabaseCredentials(ldbName, false) != null)
{
// If a configuration is already set up for this (pdb/ldb) combination, abort
errorSink.add("A configuration is already set up for this database.");
return false;
}
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getInsertTenantDatabases()))
{
stmt.setString(1, tenantName);
stmt.setString(2, pdbName);
stmt.setString(3, ldbName);
stmt.setString(4, db.getUrl());
stmt.setString(5, db.getUsername());
stmt.setString(6, encrypt(db.getPassword()));
setIntValueInStatement(stmt, 7, db.getC3p0MaxStmts());
setIntValueInStatement(stmt, 8, db.getC3p0MinPool());
setIntValueInStatement(stmt, 9, db.getC3p0MaxPool());
setIntValueInStatement(stmt, 10, db.getC3p0AcqInc());
setIntValueInStatement(stmt, 11, db.getC3p0MaxIdle());
stmt.execute();
}
// no catch here, let the outer [try] catch it
tenant.addDatabaseInstance(pdbName, db);
String err = DatabaseManager.activateTenantDatabase(tenant, pdbName);
if (err != null)
{
errorSink.add("Failed to activate tenant database due to " + err);
// be more lenient: if the logical database was not yet connected, just warn the event:
LOG.log(Level.WARNING, "Failed to activate tenant database due to " + err);
}
for (TenantMetaChangeListener mt : metaListeners)
{
if (mt.getDatabase().equals(ldbName))
{
runInGlobalBlock(() -> mt.addNewTenant(tenant, errorSink));
}
}
conn.commit();
return true;
}
catch (SQLException | PersistenceException e)
{
if (conn != null)
{
try
{
conn.rollback();
}
catch (SQLException ex)
{
// will be logged/reported anyway
}
}
errorSink.add("Cannot add database to tenant due to " + e.getMessage());
LOG.log(Level.WARNING, "Cannot add database to tenant due to " + e.getMessage(), e);
errorReported = true;
}
finally
{
if (conn != null)
{
try
{
conn.close();
}
catch (SQLException e)
{
if (!errorReported) // avoid duplicating error messages
{
errorSink.add("Cannot add database to tenant due to " + e.getMessage());
LOG.log(Level.WARNING, "Cannot add database to tenant due to " + e.getMessage(), e);
}
}
}
}
}
return false;
}
/**
* Add a database to the default tenant.
*
* @param db
* The database descriptor which stores information about the database to be added.
*/
public static void addDatabaseToDefaultTenant(TenantDatabaseDescriptor db)
{
if (!init)
{
return;
}
synchronized (lock)
{
Tenant defaultTenant = tenants.get(DEFAULT_TENANT_NAME);
if (defaultTenant == null)
{
LOG.log(Level.WARNING, "Default tenant is not instantiated.");
return;
}
String pdbName = db.getPdbName();
if (defaultTenant.getDatabaseCredentials(pdbName, true) != null)
{
// If a configuration is already set up for this database, abort
return;
}
defaultTenant.addDatabaseInstance(pdbName, db);
}
}
/**
* Get a list with all the domains of a logical database.
*
* @param ldbName
* The logical database name whose domains are requested.
* @param errorSink
* A sink for errors encountered.
*
* @return A list with the registered domains from {@code _sec-authentication-domain} meta table of the
* specified logical database.
*/
public static ArrayList<String> listDomains(String ldbName, List<String> errorSink)
{
return listSecAuthTable(ldbName, SEC_AUTHENTICATION_DOMAIN, "_Domain-name", errorSink);
}
/**
* Get a list with all the domain types defined in a logical database.
*
* @param ldbName
* The logical database name whose domain types are requested.
* @param errorSink
* A sink for errors encountered.
*
* @return A list with the registered domains from {@code _sec-authentication-system} meta table of the
* specified logical database.
*/
public static ArrayList<String> listDomainTypes(String ldbName, List<String> errorSink)
{
return listSecAuthTable(ldbName, SEC_AUTHENTICATION_SYSTEM, "_Domain-type", errorSink);
}
/**
* Generic implementation for extracting the list of a keys from a specified (meta-) table.
*
* @param ldbName
* The logical database name whose domain types are requested.
* @param legacyName
* The legacy name of the meta-table.
* @param keyField
* The field which acts as a key and identifies unique records.
* @param errorSink
* A sink for errors encountered.
*
* @return A list with the registered domains from {@code _sec-authentication-*} meta table of the
* specified logical database.
*/
private static ArrayList<String> listSecAuthTable(String ldbName,
String legacyName,
String keyField,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return null;
}
Persistence p;
try
{
p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
}
catch (Exception e)
{
errorSink.add(e.getMessage());
// some error happened. [ldbName] may be invalid
return null;
}
if (p == null)
{
errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
return null;
}
ArrayList<String> res = new ArrayList<>();
runInGlobalBlock(() -> {
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
handle bh = UndoableFactory.handle();
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "qh")
handle qh = UndoableFactory.handle();
doBlock(TransactionType.NONE, "listDomainsLabel", new Block((Body) () ->
{
try
{
// create the buffer for the _Domains meta table
RecordBuffer.createMeta(bh, p, legacyName /*"_sec-authentication-domain/system"*/, ldbName);
QueryWrapper.createQuery(qh);
P2JQuery query = (P2JQuery) qh.getResource();
query.setBuffers(bh);
query.prepare("FOR EACH " + ldbName + "." + legacyName + " NO-LOCK");
query.queryOpen();
query.getFirst();
BufferImpl domain = (BufferImpl) bh.getResource();
while (domain.available().booleanValue())
{
res.add(domain.dereference(keyField).toStringMessage()); // it's a character
query.getNext();
}
}
catch (Throwable t)
{
errorSink.add(t.getMessage());
undoLeave(); // ignore it? success[0] remains false
}
}));
// cleanup:
HandleOps.delete(bh);
HandleOps.delete(qh);
});
return res;
}
/**
* Get the details for a specific domain of a logical database.
*
* @param ldbName
* The logical database name whose domain are requested.
* @param domainName
* The domain name whose details are requested.
* @param errorSink
* A sink for errors encountered.
*
* @return A map with details of a registered domain from {@code _sec-authentication-domain} meta table of
* the specified logical database.
*/
public static HashMap<String, BaseDataType> getDomain(String ldbName,
String domainName,
List<String> errorSink)
{
return getSecAuthRecord(ldbName, domainName, SEC_AUTHENTICATION_DOMAIN, "_Domain-name", errorSink);
}
/**
* Get the details for a specific domain type from a logical database.
*
* @param ldbName
* The logical database name whose domain are requested.
* @param domainTypeName
* The name of domain type whose details are requested.
* @param errorSink
* A sink for errors encountered.
*
* @return A map with details of a registered domain from {@code _sec-authentication-domain} meta table of
* the specified logical database.
*/
public static HashMap<String, BaseDataType> getDomainType(String ldbName,
String domainTypeName,
List<String> errorSink)
{
return getSecAuthRecord(ldbName, domainTypeName, SEC_AUTHENTICATION_SYSTEM, "_Domain-type", errorSink);
}
/**
* Get the details for a specific domain or domain-type from the logical database.
*
* @param ldbName
* The logical database name whose domain are requested.
* @param keyName
* The domain/domainType name whose details are requested.
* @param legacyName
* The legacy name of the meta-table.
* @param keyField
* The field which acts as a key and identifies unique records.
* @param errorSink
* A sink for errors encountered.
*
* @return A map with details of a registered domain from {@code _sec-authentication-*} meta table of
* the specified logical database.
*/
private static HashMap<String, BaseDataType> getSecAuthRecord(String ldbName,
String keyName,
String legacyName,
String keyField,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return null;
}
if (!validateName(ldbName) || !validateName(keyName))
{
errorSink.add("Invalid parameters.");
return new HashMap<>();
}
Persistence p;
try
{
p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
}
catch (Exception e)
{
errorSink.add(e.getMessage());
// some error happened. [ldbName] may be invalid
return null;
}
if (p == null)
{
errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
return null;
}
HashMap<String, BaseDataType> res = new HashMap<>();
runInGlobalBlock(() -> {
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
handle bh = UndoableFactory.handle();
doBlock(TransactionType.NONE, "getDomainLabel", new Block((Body) () ->
{
try
{
// create the buffer for the _Domains meta table
RecordBuffer.createMeta(bh, p, legacyName /*"_sec-authentication-domain/system"*/, ldbName);
BufferImpl domain = (BufferImpl) bh.getResource();
domain.findFirst(new character("WHERE " + keyField + " = '" + keyName + "'"));
if (domain.available().booleanValue())
{
DmoMeta dmoInfo = domain.buffer().getDmoInfo();
Iterator<Property> fields = dmoInfo.getFields(false);
while (fields.hasNext())
{
Property prop = fields.next();
res.put(prop.legacy, domain.dereference(prop.legacy));
}
}
else
{
errorSink.add(keyField + " resource '" + keyName + "' not found.");
}
}
catch (Throwable t)
{
errorSink.add(t.getMessage());
undoLeave(); // ignore it? success[0] remains false
}
}));
// cleanup:
HandleOps.delete(bh);
});
return res;
}
/**
* Add a domain to the list.
*
* @param ldbName
* The logical database name where the new domains is about to be created.
* @param properties
* The properties of the new domain, including the name
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation is performed successfully,
* {@code false} if the operation is not performed successfully,
*/
public static boolean addDomain(String ldbName, JsonNode properties, List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
Persistence p;
try
{
p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
}
catch (Exception e)
{
errorSink.add(e.getMessage());
// some error happened. [ldbName] may be invalid
return false;
}
if (p == null)
{
errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
return false;
}
Boolean[] success = new Boolean[] { false };
runInGlobalBlock(() -> {
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
handle bh = UndoableFactory.handle();
doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
{
try
{
// create the buffer for the _Domains meta table
RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_DOMAIN, ldbName);
BufferImpl domain = (BufferImpl) bh.getResource();
logical ok = domain.bufferCreate();
if (ok.booleanValue())
{
setField(domain, properties, "_Domain-name", JsonNode::textValue);
setField(domain, properties, "_Domain-type", JsonNode::textValue);
setField(domain, properties, "_Domain-description", JsonNode::textValue);
setField(domain, properties, "_Domain-access-code", JsonNode::textValue);
setField(domain, properties, "_Auditing-context", JsonNode::textValue);
setField(domain, properties, "_Domain-runtime-options", JsonNode::textValue);
setField(domain, properties, "_Domain-enabled", JsonNode::booleanValue);
setField(domain, properties, "_PAM-actions", JsonNode::textValue);
setField(domain, properties, "_PAM-control", JsonNode::textValue);
setField(domain, properties, "_PAM-library-path", JsonNode::textValue);
setField(domain, properties, "_PAM-options", JsonNode::textValue);
setField(domain, properties, "_Domain-custom-detail", JsonNode::textValue);
setField(domain, properties, "_Tenant-Name", JsonNode::textValue);
setField(domain, properties, "_Domain-Id", JsonNode::longValue);
setField(domain, properties, "_Domain-category", JsonNode::intValue);
domain.release();
success[0] = true;
SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_DOMAIN);
}
else
{
errorSink.add("Resource '" + SEC_AUTHENTICATION_DOMAIN + "' could not be created.");
}
}
catch (Throwable t)
{
errorSink.add(t.getMessage());
undoLeave(); // ignore it? success[0] remains false
}
}));
// cleanup:
if (bh._isValid())
{
HandleOps.delete(bh);
}
});
return success[0];
}
/**
* Add a domain to the list.
*
* @param ldbName
* The logical database name where the new domains is about to be created.
* @param properties
* The properties of the new domain, including the name
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation is performed successfully,
* {@code false} if the operation is not performed successfully,
*/
public static boolean addDomainType(String ldbName, JsonNode properties, List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
Persistence p;
try
{
p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
}
catch (Exception e)
{
errorSink.add(e.getMessage());
// some error happened. [ldbName] may be invalid
return false;
}
if (p == null)
{
errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
return false;
}
Boolean[] success = new Boolean[] { false };
runInGlobalBlock(() -> {
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
handle bh = UndoableFactory.handle();
doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
{
try
{
// create the buffer for the _Domains meta table
RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_SYSTEM, ldbName);
BufferImpl domainType = (BufferImpl) bh.getResource();
logical ok = domainType.bufferCreate();
if (ok.booleanValue())
{
setField(domainType, properties, "_Domain-type", JsonNode::textValue);
setField(domainType, properties, "_Domain-type-description", JsonNode::textValue);
setField(domainType, properties, "_PAM-plug-in", JsonNode::booleanValue);
setField(domainType, properties, "_PAM-module-name", JsonNode::textValue);
setField(domainType, properties, "_PAM-callback-procedure", JsonNode::textValue);
setField(domainType, properties, "_Key-store-id", JsonNode::textValue);
setField(domainType, properties, "_Custom-detail", JsonNode::textValue);
setField(domainType, properties, "_PAM-library-checksum",
jsonNode -> new raw(Base64.decodeBase64(jsonNode.textValue())));
domainType.release();
success[0] = true;
SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_SYSTEM);
}
else
{
errorSink.add("Resource '" + SEC_AUTHENTICATION_SYSTEM + "' could not be created.");
}
}
catch (Throwable t)
{
errorSink.add(t.getMessage());
undoLeave(); // ignore it? success[0] remains false
}
}));
// cleanup:
if (bh._isValid())
{
HandleOps.delete(bh);
}
});
return success[0];
}
/**
* Update a domain from the master table.
*
* @param ldbName
* The logical database name whose domain is updated.
* @param domainName
* The name of the domain that is targeted by the update.
* @param properties
* A Map with pairs (column name - new value) that are intended for the update.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if all the updates are performed successfully,
* {@code false} if there is at least an update that fails,
* {@code null} if the caller does not have sufficient rights.
*/
public static Boolean updateDomain(String ldbName,
String domainName,
JsonNode properties,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (!validateName(ldbName) || !validateName(domainName))
{
errorSink.add("Invalid parameters.");
return false;
}
Persistence p;
try
{
p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
}
catch (Exception e)
{
errorSink.add(e.getMessage());
// some error happened. [ldbName] may be invalid
return false;
}
if (p == null)
{
errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
return false;
}
Boolean[] success = new Boolean[] { false };
runInGlobalBlock(() -> {
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
handle bh = UndoableFactory.handle();
doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
{
try
{
RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_DOMAIN, ldbName); // create the buffer for the _Domains meta table
BufferImpl domain = (BufferImpl) bh.getResource();
logical ok = domain.findFirst(new character("WHERE _Domain-name = '" + domainName + "'"),
LockType.EXCLUSIVE);
if (ok.booleanValue())
{
setField(domain, properties, "_Domain-name", JsonNode::textValue);
setField(domain, properties, "_Domain-type", JsonNode::textValue);
setField(domain, properties, "_Domain-description", JsonNode::textValue);
setField(domain, properties, "_Domain-access-code", JsonNode::textValue);
setField(domain, properties, "_Auditing-context", JsonNode::textValue);
setField(domain, properties, "_Domain-runtime-options", JsonNode::textValue);
setField(domain, properties, "_Domain-enabled", JsonNode::booleanValue);
setField(domain, properties, "_PAM-actions", JsonNode::textValue);
setField(domain, properties, "_PAM-control", JsonNode::textValue);
setField(domain, properties, "_PAM-library-path", JsonNode::textValue);
setField(domain, properties, "_PAM-options", JsonNode::textValue);
setField(domain, properties, "_Domain-custom-detail", JsonNode::textValue);
setField(domain, properties, "_Tenant-Name", JsonNode::textValue);
setField(domain, properties, "_Domain-Id", JsonNode::longValue);
setField(domain, properties, "_Domain-category", JsonNode::intValue);
domain.release();
success[0] = true;
SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_DOMAIN);
}
else
{
errorSink.add(SEC_AUTHENTICATION_DOMAIN + " resource '" + domainName + "' not found.");
}
}
catch (Throwable t)
{
errorSink.add(t.getMessage());
undoLeave(); // ignore it? success[0] remains false
}
}));
// cleanup:
HandleOps.delete(bh);
});
return success[0];
}
/**
* Update a domain type from the master table.
*
* @param ldbName
* The logical database name whose domain is updated.
* @param domainType
* The domain type that is targeted by the update.
* @param properties
* A Map with pairs (column name - new value) that are intended for the update.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if all the updates are performed successfully,
* {@code false} if there is at least an update that fails,
* {@code null} if the caller does not have sufficient rights.
*/
public static Boolean updateDomainType(String ldbName,
String domainType,
JsonNode properties,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (!validateName(ldbName) || !validateName(domainType))
{
errorSink.add("Invalid parameters.");
return false;
}
Persistence p;
try
{
p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
}
catch (Exception e)
{
errorSink.add(e.getMessage());
// some error happened. [ldbName] may be invalid
return false;
}
if (p == null)
{
errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
return false;
}
Boolean[] success = new Boolean[] { false };
runInGlobalBlock(() -> {
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
handle bh = UndoableFactory.handle();
doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
{
try
{
// create the buffer for the _Domains meta table
RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_SYSTEM, ldbName);
BufferImpl domain = (BufferImpl) bh.getResource();
logical ok = domain.findFirst(new character("WHERE _Domain-type = '" + domainType + "'"),
LockType.EXCLUSIVE);
if (ok.booleanValue())
{
setField(domain, properties, "_Domain-type", JsonNode::textValue);
setField(domain, properties, "_Domain-type-description", JsonNode::textValue);
setField(domain, properties, "_PAM-plug-in", JsonNode::booleanValue);
setField(domain, properties, "_PAM-module-name", JsonNode::textValue);
setField(domain, properties, "_PAM-callback-procedure", JsonNode::textValue);
setField(domain, properties, "_Key-store-id", JsonNode::textValue);
setField(domain, properties, "_Custom-detail", JsonNode::textValue);
setField(domain, properties, "_PAM-library-checksum",
jsonNode -> new raw(Base64.decodeBase64(jsonNode.textValue())));
domain.release();
success[0] = true;
SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_SYSTEM);
}
else
{
errorSink.add(SEC_AUTHENTICATION_SYSTEM + " resource '" + domainType + "' not found.");
}
}
catch (Throwable t)
{
errorSink.add(t.getMessage());
undoLeave(); // ignore it? success[0] remains false
}
}));
// cleanup:
HandleOps.delete(bh);
});
return success[0];
}
/**
* Function for deleting a domain from a logical database.
*
* @param ldbName
* The logical database name whose domain is about to be deleted.
* @param domainName
* The name of the tenant that is to be deleted.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation was successful, {@code false} otherwise.
*/
public static boolean deleteDomain(String ldbName, String domainName, List<String> errorSink)
{
return deleteSecAuthRecord(ldbName, domainName, SEC_AUTHENTICATION_DOMAIN, "_Domain-name", errorSink);
}
/**
* Function for deleting a domain from a logical database.
*
* @param ldbName
* The logical database name whose domain is about to be deleted.
* @param domainName
* The name of the tenant that is to be deleted.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation was successful, {@code false} otherwise.
*/
public static boolean deleteDomainType(String ldbName, String domainName, List<String> errorSink)
{
return deleteSecAuthRecord(ldbName, domainName, SEC_AUTHENTICATION_SYSTEM, "_Domain-type", errorSink);
}
/**
* Function for deleting a record from a sec_auth table of a logical database.
*
* @param ldbName
* The logical database name whose domain is about to be deleted.
* @param recordKey
* The key name of the record to be deleted.
* @param legacyName
* The legacy name of the meta-table.
* @param keyField
* The field which acts as a key and identifies unique records.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation was successful, {@code false} otherwise.
*/
public static boolean deleteSecAuthRecord(String ldbName,
String recordKey,
String legacyName,
String keyField,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (!validateName(ldbName) || !validateName(recordKey))
{
errorSink.add("Invalid parameters.");
return false;
}
Persistence p;
try
{
p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
}
catch (Exception e)
{
errorSink.add(e.getMessage());
// some error happened. [ldbName] may be invalid
return false;
}
if (p == null)
{
errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
return false;
}
Boolean[] success = new Boolean[] { false };
runInGlobalBlock(() -> {
@LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
handle bh = UndoableFactory.handle();
doBlock(TransactionType.FULL, "deleteDomainsLabel", new Block((Body) () ->
{
try
{
// create the buffer for the _Domains meta table
RecordBuffer.createMeta(bh, p, legacyName /*"_sec-authentication-domain/system"*/, ldbName);
BufferImpl domain = (BufferImpl) bh.getResource();
logical ok = domain.findFirst(new character("WHERE " + keyField + " = '" + recordKey + "'"),
LockType.EXCLUSIVE);
if (ok.booleanValue())
{
domain.deleteRecord();
success[0] = true;
SecurityOps.invalidate(ldbName, legacyName);
}
else
{
errorSink.add(legacyName + " resource '" + recordKey + "' not found.");
}
}
catch (Throwable t)
{
errorSink.add(t.getMessage());
undoLeave(); // ignore it? success[0] remains false
}
}));
// cleanup:
HandleOps.delete(bh);
});
return success[0];
}
/**
* Update a tenant from the master table. This method allows one or multiple column values to be edited.
* Possible {@code column} values/names are:
* <ul>
* <li> tenant_name - the name of the tenant.
* <li> db_name - the name of a database instance that this tenant uses.
* <li> url - the URL of a database instance.
* <li> username - the username required in order to access a database instance.
* <li> password - the password required in order to access a database instance.
* </ul>
*
* @param tenantName
* The name of the tenant that is targeted by the update.
* @param pdbName
* The physical name of the database that is targeted by the update.
* @param values
* A Map with pairs (column name - new value) that are intended for the update.
* @param databaseUpdate
* Is this update targeting a database change?
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if all the updates are performed successfully,
* {@code false} if there is at least an update that fails.
*/
public static boolean updateTenant(String tenantName,
String pdbName,
Map<String, String> values,
boolean databaseUpdate,
List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (tenantName.equals(DEFAULT_TENANT_NAME))
{
errorSink.add("Cannot update the default tenant.");
LOG.log(Level.WARNING, "Cannot update the default tenant.");
return false;
}
synchronized (lock)
{
Tenant tenant = tenants.get(tenantName);
if (tenant == null)
{
errorSink.add("Cannot update a non-existing tenant.");
LOG.log(Level.WARNING, "Cannot update a non-existing tenant.");
return false;
}
if (databaseUpdate && tenant.getDatabaseCredentials(pdbName, true) == null)
{
errorSink.add("Cannot update a non-existing database instance.");
LOG.log(Level.WARNING, "Cannot update a non-existing database instance.");
return false;
}
Connection conn = null;
boolean errorReported = false;
try
{
conn = getConnection(true);
boolean success = true;
for (Map.Entry<String, String> entry : values.entrySet())
{
String field = determineDatabaseFieldName(entry.getKey());
String value = entry.getValue();
if (!updateTenantImpl(conn, tenantName, pdbName, field, value, databaseUpdate))
{
success = false;
}
else
{
tenant.setField(pdbName, field, value);
}
}
if (databaseUpdate)
{
DatabaseManager.removeTenantDbConfig(tenantName, pdbName);
}
if (success)
{
for (TenantMetaChangeListener mt : metaListeners)
{
// if (mt.getDatabase().equals(db.getPdbName()))
{
runInGlobalBlock(() -> mt.tenantChanged(tenantName, tenant, errorSink));
}
}
}
conn.commit();
return success;
}
catch (SQLException e)
{
if (conn != null)
{
try
{
conn.rollback();
}
catch (SQLException ex)
{
// will be logged/reported anyway
}
}
LOG.log(Level.WARNING, "Could not update tenant due to " + e.getMessage(), e);
errorSink.add("Could not update tenant due to " + e.getMessage());
errorReported = true;
}
finally
{
if (conn != null)
{
try
{
conn.close();
}
catch (SQLException e)
{
if (!errorReported) // avoid duplicating error messages
{
LOG.log(Level.WARNING, "Could not update tenant due to " + e.getMessage(), e);
errorSink.add("Could not update tenant due to " + e.getMessage());
}
}
}
}
}
return false;
}
/**
* Function for deleting a tenant from the master table.
*
* @param tenantName
* The name of the tenant that is targeted by the delete.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation was successful, {@code false} otherwise.
*/
public static boolean deleteTenant(String tenantName, List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
synchronized (lock)
{
Connection conn = null;
boolean errorReported = false;
try
{
conn = getConnection(true);
if (tenantName.equals(DEFAULT_TENANT_NAME))
{
errorSink.add("Cannot delete the default tenant.");
LOG.log(Level.WARNING, "Cannot delete the default tenant.");
return false;
}
Tenant tenant = tenants.get(tenantName);
if (tenant == null || tenant.id == DEFAULT_TENANT_ID)
{
errorSink.add("Cannot delete a non-existing tenant.");
LOG.log(Level.WARNING, "Cannot delete a non-existing tenant.");
return false;
}
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getSelectTenantDatabases()))
{
stmt.setString(1, tenantName);
try (ResultSet rs = stmt.executeQuery())
{
while (rs.next())
{
String pdbName = rs.getString(COL_PHYSICAL_DB);
TenantDatabaseDescriptor dbData = tenant.getDatabaseCredentials(pdbName, true);
Database pdb = new Database(dbData.getLdbName(), Database.Type.PRIMARY, true);
Database db = pdb.createTenant(dbData.getPdbName());
DatabaseManager.removeTenantDbFromLockTable(db);
Settings.removeSettings(db);
FastFindCache.removeTenantCache(db);
String ldbName = dbData.getLdbName();
ConnectionManager.get().removeTenant(ldbName);
for (TenantMetaChangeListener mt : metaListeners)
{
if (mt.getDatabase().equals(dbData.getLdbName()))
{
runInGlobalBlock(() -> mt.deleteTenant(tenant, errorSink));
}
}
}
}
}
// delete the databases of the tenant
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getDeleteTenantDatabases()))
{
stmt.setString(1, tenantName);
stmt.execute();
}
// delete the tenant from landlord database
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getDeleteTenant()))
{
stmt.setString(1, tenantName);
stmt.execute();
}
tenants.remove(tenantName);
tenantsById.remove(Math.abs(tenant.id));
DatabaseManager.removeTenantDbConfigs(tenantName);
conn.commit();
return true;
}
catch (SQLException e)
{
if (conn != null)
{
try
{
conn.rollback();
}
catch (SQLException ex)
{
// will be logged/reported anyway
}
}
errorSink.add("Could not delete the tenant due to " + e.getMessage());
LOG.log(Level.WARNING, "Could not delete the tenant due to " + e.getMessage(), e);
errorReported = true;
}
finally
{
if (conn != null)
{
try
{
conn.close();
}
catch (SQLException e)
{
if (!errorReported) // avoid duplicating error messages
{
errorSink.add("Could not delete the tenant due to " + e.getMessage());
LOG.log(Level.WARNING, "Could not delete the tenant due to " + e.getMessage(), e);
}
}
}
}
}
return false;
}
/**
* Delete a database instance that is assigned to a tenant.
*
* @param tenantName
* The tenant for which to remove the database instance.
* @param pdbName
* The database physical name of the database to be deleted.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation was successful, {@code false} otherwise.
*/
public static boolean deleteDatabaseFromTenant(String tenantName, String pdbName, List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (tenantName.equals(DEFAULT_TENANT_NAME))
{
errorSink.add("Cannot delete database instances of the default tenant.");
LOG.log(Level.WARNING, "Cannot delete database instances of the default tenant.");
return false;
}
synchronized (lock)
{
Tenant tenant = tenants.get(tenantName);
if (tenant == null)
{
errorSink.add("Cannot delete a database of a non-existing tenant.");
LOG.log(Level.WARNING, "Cannot delete a database of a non-existing tenant.");
return false;
}
TenantDatabaseDescriptor dbDescriptor = tenant.getDatabaseCredentials(pdbName, true);
if (dbDescriptor == null)
{
errorSink.add("The database to be deleted not found in the db list of the specified tenant.");
LOG.log(Level.WARNING,
"The database to be deleted not found in the databases list of the specified tenant.");
return false;
}
Database pdb = new Database(dbDescriptor.getLdbName(), Database.Type.PRIMARY, true);
Database db = pdb.createTenant(dbDescriptor.getPdbName());
FastFindCache.removeTenantCache(db);
Connection conn = null;
boolean errorReported = false;
try
{
conn = getConnection(true);
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getDeleteTenantDatabase()))
{
stmt.setString(1, tenantName);
stmt.setString(2, pdbName);
stmt.execute();
}
// no catch here, let the outer [try] catch it
tenant.removeDatabaseInstance(pdbName);
Settings.removeSettings(db);
DatabaseManager.removeTenantDbConfig(tenantName, pdbName);
DatabaseManager.removeTenantDbFromLockTable(db);
String ldbName = dbDescriptor.getLdbName();
ConnectionManager.get().removeTenant(ldbName);
// delete tenant from _Tenant
for (TenantMetaChangeListener mt : metaListeners)
{
if (mt.getDatabase().equals(ldbName))
{
runInGlobalBlock(() -> mt.deleteTenant(tenant, errorSink));
}
}
conn.commit();
return true;
}
catch (SQLException e)
{
if (conn != null)
{
try
{
conn.rollback();
}
catch (SQLException ex)
{
// will be logged/reported anyway
}
}
errorSink.add("Failed to delete the database of the tenant due to " + e.getMessage());
LOG.log(Level.WARNING, "Failed to delete the database of the tenant due to " + e.getMessage());
errorReported = true;
}
finally
{
if (conn != null)
{
try
{
conn.close();
}
catch (SQLException e)
{
if (!errorReported) // avoid duplicating error messages
{
errorSink.add("Failed to close the connection to landlord database due to " +
e.getMessage());
LOG.log(Level.WARNING,
"Failed to close the connection to landlord database due to " + e.getMessage(),
e);
}
}
}
}
}
return false;
}
/**
* Enable/Disable a specific tenant.
*
* @param tenantName
* The tenant to work with.
* @param enable
* {@code true} if the intent is to enable the tenant, {@code false} otherwise.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the operation is performed successfully, {@code false} otherwise.
*/
public static boolean changeTenantStatus(String tenantName, boolean enable, List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (tenantName.equals(DEFAULT_TENANT_NAME))
{
LOG.log(Level.WARNING, "Cannot change the status of the default tenant.");
errorSink.add("Cannot change the status of the default tenant.");
return false;
}
synchronized (lock)
{
if (!tenants.containsKey(tenantName))
{
LOG.log(Level.WARNING, "Cannot change the status of a non-existing tenant.");
errorSink.add("Cannot change the status of a non-existing tenant.");
return false;
}
try (Connection conn = getConnection(false)) // no transaction, simple setter
{
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getEnableTenant()))
{
stmt.setBoolean(1, enable);
stmt.setString(2, tenantName);
stmt.execute();
}
// no catch here, let the outer [try] catch it
Tenant tenant = tenants.get(tenantName);
tenant.setStatus(enable);
for (TenantMetaChangeListener mt : metaListeners)
{
// if (mt.getDatabase().equals(db.getPdbName()))
{
runInGlobalBlock(() -> mt.tenantStateChanged(tenant, errorSink));
}
}
return true;
}
catch (SQLException e)
{
LOG.log(Level.WARNING, "Cannot enable/disable this tenant.", e);
errorSink.add("Cannot enable/disable this tenant: " + e.getMessage());
}
return false;
}
}
/**
* Check if a specific tenant is enabled or not.
*
* @param tenantName
* The tenant to work with.
* @param errorSink
* A sink for errors encountered.
*
* @return {@code true} if the tenant is enabled, {@code false} otherwise.
*/
public static boolean isTenantEnabled(String tenantName, List<String> errorSink)
{
if (!init)
{
errorSink.add("Tenant Manager not initialized.");
return false;
}
if (tenantName.equals(DEFAULT_TENANT_NAME))
{
LOG.log(Level.WARNING, "Cannot query the status of the default tenant.");
errorSink.add("Cannot query the status of the default tenant.");
return false;
}
synchronized (lock)
{
if (!tenants.containsKey(tenantName))
{
LOG.log(Level.WARNING, "Cannot query the status of a non-existing tenant.");
errorSink.add("Cannot query the status of a non-existing tenant.");
return false;
}
try (Connection conn = getConnection(false)) // no transaction, simple getter
{
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getIsTenantEnabled()))
{
stmt.setString(1, tenantName);
try (ResultSet rs = stmt.executeQuery())
{
return rs.next() && rs.getBoolean(1);
}
// no catch here, let the outer [try] catch it
}
// no catch here, let the outer [try] catch it
}
catch (SQLException e)
{
LOG.log(Level.WARNING, "Failed to find the tenant because " + e.getMessage(), e);
errorSink.add("Failed to find the tenant because " + e.getMessage());
}
return false;
}
}
/**
* Get a tenant from the tenants map.
*
* @param tenantName
* The name of the tenant.
* @param errorSink
* A sink for errors encountered.
*
* @return The instance of {@code Tenant} that holds other information.
*/
public static Tenant getTenant(String tenantName, List<String> errorSink)
{
if (!init)
{
if (errorSink != null)
{
errorSink.add("Tenant Manager not initialized.");
}
return null;
}
synchronized (lock)
{
Tenant tenant = tenants.get(tenantName);
if (tenant == null)
{
if (errorSink != null)
{
errorSink.add("Could not find the tenant " + tenantName);
}
LOG.log(Level.WARNING, "Could not find the tenant " + tenantName);
return null;
}
return new Tenant(tenant);
}
}
/**
* Get the database instance of a tenant based on the tenant name and the database physical name.
*
* @param tenantName
* The name of the tenant for which to retrieve the database.
* @param dbName
* The name of the database instance to be retrieved.
* @param physical
* {@code true} if the given database name is the physical one,
* {@code false} if the database name is the logical one.
*
* @return The database instance, if it exists.
*/
public static Database getDatabase(String tenantName, String dbName, boolean physical, Database.Type type)
{
if (!init)
{
return null;
}
synchronized (lock)
{
Tenant tenant = tenants.get(tenantName);
if (tenant == null)
{
LOG.log(Level.WARNING, "Cannot retrieve the database of a non-existing tenant.");
return null;
}
TenantDatabaseDescriptor dbDescriptor = tenant.getDatabaseCredentials(dbName, physical);
if (dbDescriptor == null)
{
LOG.log(Level.WARNING, "Cannot retrieve the database settings of a non-existing database.");
return null;
}
Database retDb = dbDescriptor.getDatabase();
// check if the database was connected, if not, do it now
if (retDb == null)
{
connectTenantImpl(tenant);
retDb = dbDescriptor.getDatabase();
}
if (retDb.getType() == type)
{
return retDb;
}
return retDb.toType(type);
}
}
/**
* Get the database settings of a specific database of a specific tenant.
*
* @param tenantName
* The tenant to work with.
* @param pdbName
* The database physical name for which to retrieve the settings.
*
* @return A list with the database settings.
*/
public static List<Object> getDatabaseSettings(String tenantName, String pdbName)
{
if (!init)
{
return null;
}
if (tenantName.equals(DEFAULT_TENANT_NAME))
{
LOG.log(Level.WARNING, "Cannot query the database settings of the default tenant.");
return null;
}
synchronized (lock)
{
if (!tenants.containsKey(tenantName))
{
LOG.log(Level.WARNING, "Cannot query the database settings of a non-existing tenant.");
return null;
}
try (Connection conn = getConnection(false)) // no transaction, simple getter
{
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getSelectTenantDatabase()))
{
stmt.setString(1, tenantName);
stmt.setString(2, pdbName);
List<Object> result = new ArrayList<>(3);
try (ResultSet rs = stmt.executeQuery())
{
if (rs.next())
{
result.add(rs.getString(COL_LOGICAL_DB));
result.add(rs.getString(COL_DB_URL));
result.add(rs.getString(COL_DB_USER));
result.add(decrypt(rs.getString(COL_DB_PASS)));
result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MAX_STMTS));
result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MIN_POOL));
result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MAX_POOL));
result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_ACQ_INC));
result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MAX_IDLE));
}
}
// no catch here, let the outer [try] catch it
return result;
}
// no catch here, let the outer [try] catch it
}
catch (SQLException e)
{
LOG.log(Level.WARNING,
"Failed to retrieve the tenant database settings because " + e.getMessage(), e);
}
return null;
}
}
/**
* Method that reads the domains from the authentication tables and adds them into the given context.
*
* @param ldbName
* The logical database name used when adding a domain.
* @param wa
* The context in which to add the domains.
*/
public static void readDomains(String ldbName, SecurityOps.WorkArea wa)
{
if (!init)
{
return;
}
synchronized (lock)
{
Tenant defaultTenant = tenants.get(DEFAULT_TENANT_NAME);
if (defaultTenant == null)
{
LOG.log(Level.WARNING, "Default tenant is not instantiated.");
return;
}
TenantDatabaseDescriptor dbDesc = defaultTenant.dbLogicalInstances.get(ldbName);
if (dbDesc == null)
{
LOG.log(Level.WARNING, "Default tenant does not have access to database " + ldbName);
return;
}
try (Connection conn = DriverManager.getConnection(
dbDesc.getUrl(), dbDesc.getUsername(), dbDesc.getPassword()))
{
// First add the domain types
try (ResultSet rs = conn.createStatement().executeQuery(tenantSQL.getSelectDomainTypes()))
{
while (rs.next())
{
wa.addDomainType(ldbName, rs.getString(1), rs.getString(2), true);
}
}
// Then add the domains
try (ResultSet rs = conn.createStatement().executeQuery(tenantSQL.getSelectDomains()))
{
while (rs.next())
{
wa.addDomain(ldbName,
rs.getString(1),
rs.getString(2),
rs.getString(3),
SymmetricEncryption.decrypt(rs.getString(4)),
rs.getBoolean(5),
rs.getString(6));
}
}
}
catch (SQLException e)
{
LOG.log(Level.WARNING, "Couldn't add the domains due to " + e.getMessage(), e);
}
}
}
/**
* Retrieve the tenant name from the given domain.
*
* @param domainName
* The name of the domain from which to retrieve the tenant name.
* @param ldbName
* The logical name of the database.
*
* @return The name of the tenant that was assigned to the given domain.
*/
public static String getTenantName(String domainName, String ldbName)
{
if (!init)
{
return null;
}
synchronized (lock)
{
Tenant defaultTenant = tenants.get(DEFAULT_TENANT_NAME);
if (defaultTenant == null)
{
LOG.log(Level.WARNING, "Default tenant is not instantiated.");
return null;
}
TenantDatabaseDescriptor dbDesc = defaultTenant.dbLogicalInstances.get(ldbName);
if (dbDesc == null)
{
LOG.log(Level.WARNING, "Default tenant does not have access to database " + ldbName);
return null;
}
SecurityOps.Domain domain = SecurityOps.findDomain(ldbName, domainName);
if (domain != null)
{
return domain.getTenantName();
}
}
return null;
}
/**
* Registers a new {@code TenantMetaChangeListener} for receiving tenant definition changes.
*
* @param tmcl
* The {@code TenantMetaChangeListener} to receive the notifications.
*
* @return {@code true} if the operation is successful.
*/
public static boolean registerMetaListener(TenantMetaChangeListener tmcl)
{
synchronized (lock)
{
return metaListeners.add(tmcl);
}
}
/**
* Deregister an existing {@code TenantMetaChangeListener} so it won't receive tenant definition changes.
*
* @param tmcl
* The {@code TenantMetaChangeListener} to receive the notifications.
*
* @return {@code true} if the operation is successful, if {@code false} the listener was not registered
* before.
*/
public static boolean deregisterMetaListener(TenantMetaChangeListener tmcl)
{
synchronized (lock)
{
return metaListeners.remove(tmcl);
}
}
/**
* Encrypt a plain password. There are a few steps executed: <ol>
* <li>a random-length prefix is added to the plain text to add extra entropy;
* <li>the resulting string is encoded to a byte array using configured algorithm and key;
* <li>the resulting byte array is converted to a hex string for easier storage in database.
* </pl>
*
* @param plainPwd
* The decrypted password.
*
* @return The encrypted password.
*/
public static String encrypt(String plainPwd)
{
initCrypto();
try
{
Random rnd = new Random();
int i = rnd.nextInt(9);
StringBuilder salted = new StringBuilder(String.valueOf((char) (i + '0')));
while (i > 0)
{
salted.append((char)(rnd.nextInt('Z' - 'A') + 'a'));
i--;
}
salted.append(plainPwd);
byte[] enc = cp.process(salted.toString().getBytes(StandardCharsets.UTF_8),
Cipher.ENCRYPT_MODE, key, iv);
return String.valueOf(Hex.encodeHex(enc));
}
catch (NoSuchAlgorithmException |
NoSuchProviderException |
NoSuchPaddingException |
InvalidKeyException |
InvalidAlgorithmParameterException |
IllegalBlockSizeException |
BadPaddingException e)
{
LOG.warning("Failed to encrypt the password. " +
"The configured password, initialization vector or cypher algorithm might be invalid", e);
// keep the original (for temporary backward compatibility)
return plainPwd;
}
}
/**
* Decrypt an encrypted password. There are a few steps executed: <ol>
* <li>the provided string is decoded from the hex string;
* <li>the resulting byte array is decrypted using the configured algorithm and key;
* <li>the randomizer prefix is removed and the result returned.
* </pl>
*
* @param codePwd
* The encrypted password.
*
* @return The decrypted password.
*/
public static String decrypt(String codePwd)
{
initCrypto();
try
{
byte[] dec = cp.process(Hex.decodeHex(codePwd), Cipher.DECRYPT_MODE, key, iv);
String s = new String(dec);
int i = s.charAt(0) - '0';
return s.substring(i + 1);
}
catch (NoSuchAlgorithmException |
NoSuchProviderException |
NoSuchPaddingException |
InvalidKeyException |
InvalidAlgorithmParameterException |
IllegalBlockSizeException |
BadPaddingException |
DecoderException e)
{
LOG.warning("Failed to decrypt the password. " +
"The password, initialization vector or cypher algorithm might have changed", e);
// keep the original (for temporary backward compatibility)
return codePwd;
}
}
/**
* Initialize the tenant managing statements. Each entry of {@code updateStmts} has an SQL update statement
* string mapped by its column name.
*
* @param tenantSQL
* The dialect specific statement provider.
*/
private static void initializeStatements(Dialect.TenantSQL tenantSQL)
{
// Initialize the update tenant statements and add them to the map
updateStmts.put(COL_TENANT_NAME, tenantSQL.getUpdateTenantName());
updateStmts.put(COL_TENANT_DESC, tenantSQL.getUpdateTenantDescription());
updateStmts.put(COL_TENANT_INFO, tenantSQL.getUpdateTenantInfo());
updateStmts.put(COL_PHYSICAL_DB, tenantSQL.getUpdatePdbName());
updateStmts.put(COL_LOGICAL_DB, tenantSQL.getUpdateLdbName());
updateStmts.put(COL_DB_URL, tenantSQL.getUpdateDbUrl());
updateStmts.put(COL_DB_USER, tenantSQL.getUpdateUsername());
updateStmts.put(COL_DB_PASS, tenantSQL.getUpdatePassword());
}
/**
* Method used for executing SQLs in order to create the required tables in the {@code landlord} database.
*
* @param conn
* The connection to database.
*/
private static void createTables(Connection conn)
throws SQLException
{
try (Statement stmt = conn.createStatement())
{
stmt.execute(tenantSQL.getCreateMasterTable());
stmt.execute(tenantSQL.getCreateMasterIndex());
stmt.execute(tenantSQL.getCreateDatabasesTable());
stmt.execute(tenantSQL.getCreateTenantDbIndex());
}
}
/**
* Method used for initializing the default tenant.
*/
private static void addDefaultTenant()
{
Tenant defaultTenant = new Tenant(DEFAULT_TENANT_ID,
DEFAULT_TENANT_NAME,
DEFAULT_TENANT_DESCRIPTION,
DEFAULT_TENANT_INFO,
null);
defaultTenant.status = true;
tenants.put(defaultTenant.tenantName, defaultTenant);
tenantsById.put(DEFAULT_TENANT_ID, defaultTenant);
}
/**
* Read all tenants defined in database and populate the {@code tenants} data structure.<p>
* Do not connect the database at this moment.
*
* @param conn
* The connection to database.
*/
private static void populateAllTenants(Connection conn)
throws SQLException
{
try (ResultSet rs = conn.createStatement().executeQuery(tenantSQL.getSelectAllTenants()))
{
while (rs.next())
{
String tenantName = rs.getString(COL_TENANT_NAME);
String tenantDesc = rs.getString(COL_TENANT_DESC);
String tenantInfo = rs.getString(COL_TENANT_INFO);
Tenant tenant = tenants.get(tenantName);
if (tenant != null)
{
LOG.log(Level.INFO, "Duplicate tenant " + tenantName);
continue; // already exists. Should not happen.
}
List<TenantDatabaseDescriptor> databases = new ArrayList<>();
try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getSelectTenantDatabases()))
{
stmt.setString(1, tenantName);
try (ResultSet dbs = stmt.executeQuery())
{
while (dbs.next())
{
databases.add(new TenantDatabaseDescriptor(
dbs.getString(COL_PHYSICAL_DB),
dbs.getString(COL_LOGICAL_DB),
dbs.getString(COL_DB_URL),
dbs.getString(COL_DB_USER),
decrypt(dbs.getString(COL_DB_PASS)),
getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MAX_STMTS),
getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MIN_POOL),
getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MAX_POOL),
getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_ACQ_INC),
getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MAX_IDLE),
null));
}
}
int nextId = getNextId(false);
tenant = new Tenant(nextId,
(UUID) rs.getObject("tenant_id"),
tenantName,
tenantDesc,
tenantInfo,
databases);
tenant.setStatus(rs.getBoolean("status"));
tenants.put(tenantName, tenant);
tenantsById.put(Math.abs(nextId), tenant);
}
}
}
}
/**
* Extracts and returns an integer value of a column from a result set, taking into consideration if the
* value is {@code null}.
*
* @param rs
* The {@code ResultSet} to be queried.
* @param colName
* The column name.
*
* @return The integer value of the specified column, possible {@code null}.
*
* @throws SQLException
* In case an error occurs.
*/
private static Integer getIntegerColumn(ResultSet rs, String colName)
throws SQLException
{
int intVal = rs.getInt(colName);
return rs.wasNull() ? null : intVal;
}
/**
* Logically connect the tenant to its respective databases.
*
* @param tenant
* The tenant to be connected to the database.
*/
private static void connectTenantImpl(Tenant tenant)
{
try
{
if (!DatabaseManager.activateTenantDatabases(tenant))
{
LOG.log(Level.WARNING, "At least one database configuration failed.");
}
}
catch (PersistenceException e)
{
LOG.log(Level.WARNING,
"Could not activate/configure this tenant databases due to " + e.getMessage(), e);
}
}
/**
* Update a tenant from the master table.
*
* @param conn
* The connection to landlord database.
* @param tenantName
* The name of the tenant that is targeted by the update.
* @param pdbName
* The physical name of the database that is targeted by the update.
* @param column
* The column that will be updated.
* @param newValue
* The value that will replace the one from the table.
* @param databaseUpdate
* Is this update targeting a database change?
*
* @return {@code true} if the operation was successful, {@code false} otherwise.
*/
private static boolean updateTenantImpl(Connection conn,
String tenantName,
String pdbName,
String column,
String newValue,
boolean databaseUpdate)
{
String stmtStr = updateStmts.get(column);
if (stmtStr == null)
{
LOG.log(Level.WARNING, "Cannot find the column " + column + " in the master table");
return false;
}
try (PreparedStatement stmt = conn.prepareStatement(stmtStr))
{
if (column.equals(COL_TENANT_NAME))
{
// We need to update both the master table and the tenant_databases table
stmt.setString(1, newValue);
stmt.setString(2, tenantName);
stmt.execute();
try (PreparedStatement dbStmt = conn.prepareStatement(tenantSQL.getUpdateTenantDbName()))
{
dbStmt.setString(1, newValue);
dbStmt.setString(2, tenantName);
dbStmt.execute();
return true;
}
}
stmt.setString(1, column.equals(COL_DB_PASS) ? encrypt(newValue) : newValue);
stmt.setString(2, tenantName);
if (databaseUpdate)
{
stmt.setString(3, pdbName);
}
stmt.execute();
return true;
}
catch (SQLException sqle)
{
LOG.log(Level.WARNING, "Could not update tenant due to " + sqle.getMessage(), sqle);
return false;
}
}
/**
* Generates a new unique id for a tenant.
* <p>
* This private method is synchronized externally (make sure each time it is called happens from a
* {@code lock} synchronized block).
*
* @param superTenant
* {@code true} if the defined entity is a super-tenant.
*
* @return a new unique id for a new tenant. If this is a super-tenant, the value is negative, according
* to 4GL reference.
*/
private static int getNextId(boolean superTenant)
{
if (tenantsById.size() == MAX_TENANTS)
{
throw new RuntimeException("No more than " + MAX_TENANTS + " tenant definitions are allowed.");
}
// pick a random number
int newId;
// if it is already taken, get the next free, to avoid possible infinite loop
do
{
newId = 1 + (int) (Math.random() * MAX_TENANTS);
}
while (tenantsById.containsKey(newId));
return superTenant ? -newId : newId;
}
/**
* Given the name of a column from the tenant API, retrieve the equivalent column name from the landlord
* database.
*
* @param field
* The field name from the tenant API.
*
* @return The correct column name from the landlord database.
*/
private static String determineDatabaseFieldName(String field)
{
switch (field)
{
case JSON_TENANT_NAME:
return COL_TENANT_NAME;
case JSON_TENANT_DESCRIPTION:
return COL_TENANT_DESC;
case JSON_TENANT_INFO:
return COL_TENANT_INFO;
case JSON_PHYSICAL_NAME:
return COL_PHYSICAL_DB;
case JSON_LOGICAL_NAME:
return COL_LOGICAL_DB;
}
// The other field names don't require changes
return field;
}
/**
* Sets a specific value to a field in a record, if that exists in the properties set. If the {@code field}
* doesn't appear in the property set, it is not set.
*
* @param domain
* The record to be changed.
* @param p
* The set o properties, as a {@code JsonNode} node.
* @param field
* The legacy name of the field.
* @param conv
* The conversion used for the value.
*/
private static void setField(BufferImpl domain, JsonNode p, String field, Function<JsonNode, Object> conv)
{
if (!p.has(field))
{
return;
}
JsonNode jsonNode = p.get(field);
try
{
domain.dereference(field, conv.apply(jsonNode));
}
catch (Throwable e)
{
throw new RuntimeException(
"Failed to set " + domain.doGetName() + "." + field + "' to requested value.", e);
}
}
/**
* Initializes the cryptographic data structures with values read from server's registry.
*/
private static void initCrypto()
{
if (cp != null)
{
return;
}
synchronized (lock)
{
DirectoryService ds = DirectoryService.getInstance();
if (!ds.bind())
{
throw new RuntimeException("Directory bind failed");
}
String algorithm = "AES_CBC_128";
String keyStr = "tenant";
String ivStr = "tenant-iv";
try
{
algorithm = Utils.getDirectoryNodeString(ds,
"persistence/landlord_encryption_algorithm",
algorithm,
false);
keyStr = Utils.getDirectoryNodeString(ds,
"persistence/landlord_encryption_key",
keyStr,
false);
ivStr = Utils.getDirectoryNodeString(ds,
"persistence/landlord_encryption_iv",
ivStr,
false);
}
finally
{
ds.unbind();
}
cp = new CryptoUtils.CipherParams(algorithm);
key = cp.newKey();
byte[] keyBytes = keyStr.getBytes(StandardCharsets.UTF_8);
System.arraycopy(keyBytes, 0, key, 0, Math.min(key.length, keyBytes.length));
iv = cp.newIV();
byte[] ivBytes = ivStr.getBytes(StandardCharsets.UTF_8);
System.arraycopy(ivBytes, 0, iv, 0, Math.min(iv.length, ivBytes.length));
}
}
/**
* Basic validate of a resource name, to avoid an 4GL code injection.
*
* @param name
* The name to be validated.
*
* @return {@code true} is the name 'looks like' a valid resource identifier.
*/
private static boolean validateName(String name)
{
return name != null && // avoid basic NPE
name.indexOf(' ') == -1 && // single word
name.indexOf('.') == -1 && // no end of 4GL statement
name.indexOf('(') == -1;
}
/**
* Get a connection to landlord database. Depending on the {@code trans} parameter, the connection can
* be transactional or auto-commited. Before returning the object, it will be tested for validity. If it is
* not valid (the connectivity to database was lost), an attempt to reset is performed. The method will
* NOT do a second validity check, returning this way a possible invalid connection which will stop the
* execution with a {@code SQLException} when it will be used.
*
* @param trans
* If {@code true} the connection will be transactional, otherwise auto-committing.
*
* @return the connection to landlord database.
*
* @throws SQLException
* In case a valid connection cannot be obtained.
*/
private static Connection getConnection(boolean trans)
throws SQLException
{
Connection conn = ds.getConnection();
if (!conn.isValid(1))
{
conn.close();
ds.softResetAllUsers();
// force one more
conn = ds.getConnection();
}
conn.setAutoCommit(!trans);
return conn;
}
/**
* Executes a fragment of code inside a temporary global block.
*
* @param code
* The code to be executed.
*/
public static void runInGlobalBlock(Runnable code)
{
// the code is simplified version of {@code Agent.prepare()}. See that method for additional information
try
{
// make sure the TM is loaded before LT
TransactionManager.isHeadless();
// set this as batch mode
EnvironmentOps.setBatchMode(true);
ProcedureManager.load();
// push a global scope for keeping buffers states
TransactionManager.pushScope("temporary-global-scope",
TransactionManager.NO_TRANSACTION,
true,
true,
false,
false);
DatabaseManager.autoConnect();
code.run();
}
finally
{
// reset the context after an operation. Mandaroty to be called in pair with {@code prepare()}.
// pop the global scope, which will be pushed again in the 'prepare' method.
TransactionManager.popScope();
// reset the context
try
{
SecurityManager.getInstance().contextSm.resetContext();
}
catch (RestrictedUseException e)
{
// ignore
}
}
}
/**
* Inner class responsible with storing information about a tenant.
*/
public static class Tenant
{
/** The tenant ID. */
private final int id;
/** The tenant type. */
private final int type;
/** The tenant UID. */
private final UUID tenantId; // TODO: is this requested by customer? can we drop it?
/** The name of the tenant. */
private String tenantName;
/** The description of the tenant. */
private String tenantDescription;
/** The info of the tenant. */
private String tenantInfo;
/** A map that stores database settings for a specific database based on the database physical name. */
private final Map<String, TenantDatabaseDescriptor> dbPhysicalInstances = new HashMap<>();
/** A map that stores database settings for a specific database based on the database logical name. */
private final Map<String, TenantDatabaseDescriptor> dbLogicalInstances = new HashMap<>();
/** The status of the tenant (enabled/disabled). */
private boolean status;
/**
* Constructor that generates a random {@code UUID} for the tenant.
*
* @param id
* The tenant's id.
* @param tenantName
* The name of the tenant.
* @param tenantDescription
* The description of the tenant.
* @param tenantInfo
* The info of the tenant.
* @param databases
* A list of the databases that will be used by the tenant.
*/
private Tenant(int id,
String tenantName,
String tenantDescription,
String tenantInfo,
List<TenantDatabaseDescriptor> databases)
{
this(id, UUID.randomUUID(), tenantName, tenantDescription, tenantInfo, databases);
}
/**
* Constructor that sets all the fields required by a tenant, including the tenant id UUID.
*
* @param id
* The tenant's id.
* @param tenantId
* The UID of the tenant.
* @param tenantName
* The name of the tenant.
* @param tenantDescription
* The description of the tenant.
* @param tenantInfo
* The info of the tenant.
* @param databases
* A list of the databases that will be used by the tenant.
*/
private Tenant(int id,
UUID tenantId,
String tenantName,
String tenantDescription,
String tenantInfo,
List<TenantDatabaseDescriptor> databases)
{
this.id = id;
this.type = (id == DEFAULT_TENANT_ID) ? TYPE_DEFAULT
: (id > 0 ? TYPE_REGULAR : TYPE_SUPER);
this.tenantId = tenantId;
this.tenantName = tenantName;
this.tenantDescription = tenantDescription;
this.tenantInfo = tenantInfo;
this.status = false;
if (databases != null)
{
for (int i = 0; i < databases.size(); i++)
{
TenantDatabaseDescriptor db = databases.get(i);
dbPhysicalInstances.put(db.getPdbName(), db);
dbLogicalInstances.put(db.getLdbName(), db);
}
}
}
/**
* Copy constructor.
*
* @param other
* The other instance from which to copy the information.
*/
private Tenant(Tenant other)
{
this.id = other.id;
this.type = other.type;
this.tenantId = other.tenantId;
this.tenantName = other.tenantName;
this.tenantDescription = other.tenantDescription;
this.tenantInfo = other.tenantInfo;
this.status = other.status;
for (Map.Entry<String, TenantDatabaseDescriptor> entry : other.dbPhysicalInstances.entrySet())
{
dbPhysicalInstances.put(entry.getKey(), new TenantDatabaseDescriptor(entry.getValue()));
}
for (Map.Entry<String, TenantDatabaseDescriptor> entry : other.dbLogicalInstances.entrySet())
{
dbLogicalInstances.put(entry.getKey(), new TenantDatabaseDescriptor(entry.getValue()));
}
}
/**
* Getter for the tenant name.
*
* @return The name of this tenant.
*/
public String getTenantName()
{
return tenantName;
}
/**
* Getter for the tenant description.
*
* @return The description of this tenant.
*/
public String getTenantDescription()
{
return tenantDescription;
}
/**
* Getter for the tenant info.
*
* @return The info of this tenant.
*/
public String getTenantInfo()
{
return tenantInfo;
}
/**
* Get the database settings/credentials of a given database.
*
* @param dbName
* The name of the database for which to retrieve the information.
* @param physical
* {@code true} if the given database name is the physical one,
* {@code false} if the database name is the logical one.
*
* @return A {@code TenantDatabaseDescriptor} that stores the information for the given database.
*/
public TenantDatabaseDescriptor getDatabaseCredentials(String dbName, boolean physical)
{
return physical ? dbPhysicalInstances.get(dbName) : dbLogicalInstances.get(dbName);
}
/**
* Get all the database settings/credentials assigned to this tenant.
*
* @return A map with the information for all the databases.
*/
public Map<String, TenantDatabaseDescriptor> getDatabaseCredentials()
{
return dbPhysicalInstances;
}
/**
* Getter for the tenant ID.
*
* @return The ID used by this tenant.
*/
public int id()
{
return id;
}
/**
* Getter for the tenant type.
*
* @return The type of the tenant.
*/
public int getType()
{
return type;
}
/**
* Getter for the tenant UID.
*
* @return The UID used by this tenant.
*/
public UUID getId()
{
return tenantId;
}
/**
* Getter for the status field (enabled/disabled).
*
* @return {@code true} if the tenant is enabled, {@code false} otherwise.
*/
public boolean getStatus()
{
return status;
}
/**
* Set a specific field for this tenant.
* Possible values for the {@code fieldName} parameter are:
* <ul>
* <li> tenant_name - the name of the tenant.
* <li> tenant_description - the description of the tenant.
* <li> db_name - the name of a database that this tenant uses.
* <li> url - the URL to a database.
* <li> username - the username required in order to access a database.
* <li> password - the password required in order to access a database.
* </ul>
*
* @param pdbName
* The physical name of the database for which to perform the update.
* @param fieldName
* The name of the field to be updated.
* @param value
* The new value for the given field.
*/
public void setField(String pdbName, String fieldName, String value)
{
switch (fieldName)
{
case COL_TENANT_NAME:
this.tenantName = value;
break;
case COL_TENANT_DESC:
this.tenantDescription = value;
break;
case COL_TENANT_INFO:
this.tenantInfo = value;
break;
case COL_PHYSICAL_DB:
// Move the values from the old database name to the new one
TenantDatabaseDescriptor db = dbPhysicalInstances.remove(pdbName);
db.setPdbName(value);
this.dbPhysicalInstances.put(value, db);
break;
case COL_LOGICAL_DB:
TenantDatabaseDescriptor dbDesc = dbPhysicalInstances.get(pdbName);
String oldLdbName = dbDesc.getLdbName();
dbDesc.setLdbName(value);
this.dbLogicalInstances.put(value, dbLogicalInstances.remove(oldLdbName));
break;
case COL_DB_URL:
this.dbPhysicalInstances.get(pdbName).setUrl(value);
break;
case COL_DB_USER:
this.dbPhysicalInstances.get(pdbName).setUsername(value);
break;
case COL_DB_PASS:
this.dbPhysicalInstances.get(pdbName).setPassword(value);
break;
default:
break;
}
}
/**
* Setter for the {@code status} field.
* A value of {@code true} denotes the enabling of this tenant, while {@code false} denotes
* disabling this tenant.
*
* @param value
* The new value for the {@code status} field.
*/
public void setStatus(boolean value)
{
this.status = value;
}
/**
* Add a database configuration for this tenant.
*
* @param pdbName
* The physical name of the database.
* @param db
* The database information to be added.
*/
private void addDatabaseInstance(String pdbName, TenantDatabaseDescriptor db)
{
dbPhysicalInstances.put(pdbName, db);
dbLogicalInstances.put(db.getLdbName(), db);
}
/**
* Remove the database configuration for this tenant.
*
* @param pdbName
* The physical name of the database for which to remove the configuration.
*/
private void removeDatabaseInstance(String pdbName)
{
TenantDatabaseDescriptor db = dbPhysicalInstances.remove(pdbName);
dbLogicalInstances.remove(db.getLdbName());
}
@Override
public String toString()
{
return "Tenant (" + id + " = \"" + tenantName + "\", " + (status ? "" : "disabled") + ")";
}
}
}