All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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