dk.cloudcreate.essentials.components.distributed.fencedlock.postgresql.PostgresqlFencedLockStorage Maven / Gradle / Ivy
/*
* Copyright 2021-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dk.cloudcreate.essentials.components.distributed.fencedlock.postgresql;
import dk.cloudcreate.essentials.components.distributed.fencedlock.postgresql.jdbi.*;
import dk.cloudcreate.essentials.components.foundation.fencedlock.*;
import dk.cloudcreate.essentials.components.foundation.postgresql.PostgresqlUtil;
import dk.cloudcreate.essentials.components.foundation.transaction.jdbi.HandleAwareUnitOfWork;
import org.jdbi.v3.core.Jdbi;
import org.slf4j.*;
import java.time.OffsetDateTime;
import java.util.Optional;
import static dk.cloudcreate.essentials.shared.FailFast.requireNonNull;
import static dk.cloudcreate.essentials.shared.MessageFormatter.msg;
/**
* Postgresql specific {@link FencedLockStorage} implementation that works with the {@link PostgresqlFencedLockManager}
*
* Security
* To support customization of storage table name, the {@code fencedLocksTableName} parameter will be directly used in constructing SQL statements
* through string concatenation, which exposes the component to SQL injection attacks.
*
* It is the responsibility of the user of this component to sanitize the {@code fencedLocksTableName}
* to ensure the security of all the SQL statements generated by this component. The {@link PostgresqlFencedLockStorage} component will
* call the {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} method to validate the table name as a first line of defense.
* The {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} provides an initial layer of defense against SQL injection by applying naming conventions intended to reduce the risk of malicious input.
* However, Essentials components as well as {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} does not offer exhaustive protection, nor does it assure the complete security of the resulting SQL against SQL injection threats.
* The responsibility for implementing protective measures against SQL Injection lies exclusively with the users/developers using the Essentials components and its supporting classes.
* Users must ensure thorough sanitization and validation of API input parameters, column, table, and index names.
* Insufficient attention to these practices may leave the application vulnerable to SQL injection, potentially endangering the security and integrity of the database.
*
* It is highly recommended that the {@code fencedLocksTableName} value is only derived from a controlled and trusted source.
* To mitigate the risk of SQL injection attacks, external or untrusted inputs should never directly provide the {@code fencedLocksTableName} value.
* Failure to adequately sanitize and validate this value could expose the application to SQL injection
* vulnerabilities, compromising the security and integrity of the database.
*/
public final class PostgresqlFencedLockStorage implements FencedLockStorage {
private static final Logger log = LoggerFactory.getLogger(PostgresqlFencedLockStorage.class);
public static final long FIRST_TOKEN = 1L;
public static final long UNINITIALIZED_LOCK_TOKEN = -1L;
public static final String DEFAULT_FENCED_LOCKS_TABLE_NAME = "fenced_locks";
private final Jdbi jdbi;
private final String fencedLocksTableName;
/**
* Create an instance of the {@link PostgresqlFencedLockStorage}.
* Locks will be stored in the {@link #DEFAULT_FENCED_LOCKS_TABLE_NAME} table.
*
* @param jdbi The {@link Jdbi} instance
*/
public PostgresqlFencedLockStorage(Jdbi jdbi) {
this(jdbi, DEFAULT_FENCED_LOCKS_TABLE_NAME);
}
/**
* Create an instance of the {@link PostgresqlFencedLockStorage}
*
* @param jdbi The {@link Jdbi} instance
* @param fencedLocksTableName The user defined table name for the storage of Fenced Locks.
* Note:
* To support customization of storage table name, the {@code fencedLocksTableName} will be directly used in constructing SQL statements
* through string concatenation, which exposes the component to SQL injection attacks.
*
* Security Note:
* It is the responsibility of the user of this component to sanitize the {@code fencedLocksTableName}
* to ensure the security of all the SQL statements generated by this component. The {@link PostgresqlFencedLockStorage} component will
* call the {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} method to validate the table name as a first line of defense.
* The {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} provides an initial layer of defense against SQL injection by applying naming conventions intended to reduce the risk of malicious input.
* However, Essentials components as well as {@link PostgresqlUtil#checkIsValidTableOrColumnName(String)} does not offer exhaustive protection, nor does it assure the complete security of the resulting SQL against SQL injection threats.
* The responsibility for implementing protective measures against SQL Injection lies exclusively with the users/developers using the Essentials components and its supporting classes.
* Users must ensure thorough sanitization and validation of API input parameters, column, table, and index names.
* Insufficient attention to these practices may leave the application vulnerable to SQL injection, potentially endangering the security and integrity of the database.
*
* It is highly recommended that the {@code fencedLocksTableName} value is only derived from a controlled and trusted source.
* To mitigate the risk of SQL injection attacks, external or untrusted inputs should never directly provide the {@code fencedLocksTableName} value.
* Failure to adequately sanitize and validate this value could expose the application to SQL injection
* vulnerabilities, compromising the security and integrity of the database.
*/
public PostgresqlFencedLockStorage(Jdbi jdbi, String fencedLocksTableName) {
this.jdbi = requireNonNull(jdbi, "You must supply a jdbi instance");
this.fencedLocksTableName = requireNonNull(fencedLocksTableName, "You must supply a fencedLocksTableName instance");
PostgresqlUtil.checkIsValidTableOrColumnName(this.fencedLocksTableName);
jdbi.registerArgument(new LockNameArgumentFactory());
jdbi.registerColumnMapper(new LockNameColumnMapper());
}
@Override
public final void initializeLockStorage(DBFencedLockManager lockManager, HandleAwareUnitOfWork unitOfWork) {
PostgresqlUtil.checkIsValidTableOrColumnName(fencedLocksTableName);
unitOfWork.handle().execute("CREATE TABLE IF NOT EXISTS " + this.fencedLocksTableName + " (\n" +
"lock_name TEXT NOT NULL,\n" + // The name of the lock
"last_issued_fence_token BIGINT,\n" + // The token issued at lock_last_confirmed_ts. Every time a lock is acquired or confirmed a new token is issued (ever growing value)
"locked_by_lockmanager_instance_id TEXT,\n" + // which JVM/Bus instance acquired the lock
"lock_acquired_ts TIMESTAMP WITH TIME ZONE,\n" + // at what time did the JVM/Bus instance acquire the lock (at first acquiring the lock_last_confirmed_ts is set to lock_acquired_ts)
"lock_last_confirmed_ts TIMESTAMP WITH TIME ZONE,\n" + // when did the JVM/Bus instance that acquired the lock last confirm that it still has access to the lock
"PRIMARY KEY (lock_name)\n" +
")");
log.info("[{}] Ensured that the '{}' fenced locks table exists", lockManager.getLockManagerInstanceId(), fencedLocksTableName);
// -------------------------------------------------------------------------------
var indexName = fencedLocksTableName + "_current_token_index";
PostgresqlUtil.checkIsValidTableOrColumnName(indexName);
unitOfWork.handle().execute("CREATE INDEX IF NOT EXISTS " + indexName + " ON " + this.fencedLocksTableName + " (lock_name, last_issued_fence_token)");
log.debug("[{}] Ensured that the '{}' index on fenced locks table '{}' exists", lockManager.getLockManagerInstanceId(), indexName, fencedLocksTableName);
}
/**
* Get the name of the table where the fenced locks are stored
*
* @return the name of the table where the fenced locks are stored
*/
public final String getFencedLocksTableName() {
return fencedLocksTableName;
}
@Override
public final boolean insertLockIntoDB(DBFencedLockManager lockManager,
HandleAwareUnitOfWork unitOfWork,
DBFencedLock initialLock,
OffsetDateTime lockAcquiredAndLastConfirmedTimestamp) {
var rowsUpdated = unitOfWork.handle()
.createUpdate("INSERT INTO " + this.fencedLocksTableName + " (" +
"lock_name, last_issued_fence_token, locked_by_lockmanager_instance_id, \n" +
"lock_acquired_ts, lock_last_confirmed_ts) \n" +
" VALUES (\n" +
":lock_name, :last_issued_fence_token, :locked_by_lockmanager_instance_id, \n" +
":lock_acquired_ts, :lock_last_confirmed_ts) ON CONFLICT DO NOTHING")
.bind("lock_name", initialLock.getName())
.bind("last_issued_fence_token", getInitialTokenValue())
.bind("locked_by_lockmanager_instance_id", lockManager.getLockManagerInstanceId())
.bind("lock_acquired_ts", lockAcquiredAndLastConfirmedTimestamp)
.bind("lock_last_confirmed_ts", lockAcquiredAndLastConfirmedTimestamp)
.execute();
return rowsUpdated == 1;
}
@Override
public final boolean updateLockInDB(DBFencedLockManager lockManager,
HandleAwareUnitOfWork unitOfWork,
DBFencedLock timedOutLock,
DBFencedLock newLockReadyToBeAcquiredLocally) {
var rowsUpdated = unitOfWork.handle()
.createUpdate("UPDATE " + this.fencedLocksTableName + " SET " +
"last_issued_fence_token=:last_issued_fence_token, " +
"locked_by_lockmanager_instance_id=:locked_by_lockmanager_instance_id,\n" +
"lock_acquired_ts=:lock_acquired_ts, " +
"lock_last_confirmed_ts=:lock_last_confirmed_ts\n" +
"WHERE lock_name=:lock_name AND " +
"last_issued_fence_token=:previous_last_issued_fence_token AND " +
"lock_last_confirmed_ts=:timed_out_locks_confirmed_ts")
.bind("lock_name", timedOutLock.getName())
.bind("last_issued_fence_token", newLockReadyToBeAcquiredLocally.getCurrentToken())
.bind("locked_by_lockmanager_instance_id", newLockReadyToBeAcquiredLocally.getLockedByLockManagerInstanceId())
.bind("lock_acquired_ts", newLockReadyToBeAcquiredLocally.getLockAcquiredTimestamp())
.bind("lock_last_confirmed_ts", newLockReadyToBeAcquiredLocally.getLockLastConfirmedTimestamp())
.bind("previous_last_issued_fence_token", timedOutLock.getCurrentToken())
.bind("timed_out_locks_confirmed_ts", timedOutLock.getLockLastConfirmedTimestamp())
.execute();
return rowsUpdated == 1;
}
@Override
public final boolean confirmLockInDB(DBFencedLockManager lockManager,
HandleAwareUnitOfWork unitOfWork,
DBFencedLock fencedLock,
OffsetDateTime confirmedTimestamp) {
var rowsUpdated = unitOfWork.handle()
.createUpdate("UPDATE " + this.fencedLocksTableName + " SET " +
"lock_last_confirmed_ts=:lock_last_confirmed_ts\n" +
"WHERE lock_name=:lock_name AND " +
"last_issued_fence_token=:last_issued_fence_token AND " +
"locked_by_lockmanager_instance_id=:locked_by_lockmanager_instance_id")
.bind("lock_name", fencedLock.getName())
.bind("locked_by_lockmanager_instance_id", requireNonNull(fencedLock.getLockedByLockManagerInstanceId(), msg("[{}] getLockedByLockManagerInstanceId was NULL. Details: {}", lockManager.getLockManagerInstanceId(), fencedLock)))
.bind("lock_last_confirmed_ts", confirmedTimestamp)
.bind("last_issued_fence_token", fencedLock.getCurrentToken())
.execute();
return rowsUpdated == 1;
}
@Override
public final boolean releaseLockInDB(DBFencedLockManager lockManager,
HandleAwareUnitOfWork unitOfWork,
DBFencedLock fencedLock) {
var rowsUpdated = unitOfWork.handle()
.createUpdate("UPDATE " + this.fencedLocksTableName + " SET " +
"locked_by_lockmanager_instance_id=NULL\n" +
"WHERE lock_name=:lock_name AND " +
"last_issued_fence_token=:lock_last_issued_token")
.bind("lock_name", fencedLock.getName())
.bind("lock_last_issued_token", fencedLock.getCurrentToken())
.execute();
return rowsUpdated == 1;
}
@Override
public final Optional lookupLockInDB(DBFencedLockManager lockManager,
HandleAwareUnitOfWork unitOfWork,
LockName lockName) {
return unitOfWork.handle()
.createQuery("SELECT * FROM " + this.fencedLocksTableName + " WHERE lock_name=:lock_name")
.bind("lock_name", lockName)
.map(row -> new DBFencedLock(lockManager,
lockName,
row.getColumn("last_issued_fence_token", Long.class),
row.getColumn("locked_by_lockmanager_instance_id", String.class),
row.getColumn("lock_acquired_ts", OffsetDateTime.class),
row.getColumn("lock_last_confirmed_ts", OffsetDateTime.class)))
.findOne();
}
@Override
public final DBFencedLock createUninitializedLock(DBFencedLockManager lockManager,
LockName lockName) {
return new DBFencedLock(lockManager,
lockName,
getUninitializedTokenValue(),
null,
null,
null);
}
@Override
public final DBFencedLock createInitializedLock(DBFencedLockManager lockManager,
LockName name,
long currentToken,
String lockedByLockManagerInstanceId,
OffsetDateTime lockAcquiredTimestamp,
OffsetDateTime lockLastConfirmedTimestamp) {
return new DBFencedLock(requireNonNull(lockManager, "lockManager is null"),
requireNonNull(name, "name is null"),
currentToken,
requireNonNull(lockedByLockManagerInstanceId, "lockedByLockManagerInstanceId is null"),
requireNonNull(lockAcquiredTimestamp, "lockAcquiredTimestamp is null"),
requireNonNull(lockLastConfirmedTimestamp, "lockLastConfirmedTimestamp is null"));
}
@Override
public final Long getUninitializedTokenValue() {
return UNINITIALIZED_LOCK_TOKEN;
}
@Override
public final long getInitialTokenValue() {
return FIRST_TOKEN;
}
@Override
public final void deleteLockInDB(DBFencedLockManager lockManager,
HandleAwareUnitOfWork unitOfWork,
LockName nameOfLockToDelete) {
var rowsUpdated = unitOfWork.handle()
.createUpdate("DELETE FROM " + this.fencedLocksTableName + " WHERE lock_name = :lock_name")
.bind("lock_name", nameOfLockToDelete)
.execute();
if (rowsUpdated == 1) {
log.debug("[{}] Deleted lock '{}'", lockManager.getLockManagerInstanceId(),
nameOfLockToDelete);
}
}
@Override
public final void deleteAllLocksInDB(DBFencedLockManager lockManager,
HandleAwareUnitOfWork unitOfWork) {
var rowsUpdated = unitOfWork.handle()
.createUpdate("DELETE FROM " + this.fencedLocksTableName)
.execute();
log.debug("[{}] Deleted all {} locks", lockManager.getLockManagerInstanceId(), rowsUpdated);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy