UcpCredentialsUpdater.java

package com.maybeitssquid.rotatingsecrets.ucp;

import com.maybeitssquid.rotatingsecrets.CredentialRotationException;
import com.maybeitssquid.rotatingsecrets.UpdatableCredential;
import java.sql.SQLException;
import java.util.Arrays;
import oracle.ucp.UniversalConnectionPoolException;
import oracle.ucp.admin.UniversalConnectionPoolManager;
import oracle.ucp.admin.UniversalConnectionPoolManagerImpl;
import oracle.ucp.jdbc.PoolDataSource;

/**
 * Handles credential updates for Oracle Universal Connection Pool (UCP) data sources.
 *
 * <p>This class implements the {@link UpdatableCredential} interface to receive credential change
 * notifications from the {@link com.maybeitssquid.rotatingsecrets.CredentialsProviderService}. When
 * credentials are updated, it atomically updates the pool's username and password, then triggers a
 * pool refresh to ensure new connections use the updated credentials.
 *
 * <h2>Credential Update Process</h2>
 *
 * <ol>
 *   <li>Acquire a lock on the pool to ensure atomic credential updates
 *   <li>Update the username and password on the {@link PoolDataSource}
 *   <li>If the pool is registered with the manager, call {@link
 *       UniversalConnectionPoolManager#refreshConnectionPool(String)} to gracefully replace
 *       connections with ones using the new credentials. A pool that has not been created yet
 *       already uses the new credentials when it is created, so there is nothing to refresh
 * </ol>
 *
 * <h2>Thread Safety</h2>
 *
 * <p>This class is thread-safe. Credential updates are synchronized on the pool data source to
 * ensure atomic username/password updates. The pool refresh operation is handled asynchronously by
 * UCP.
 *
 * @see UpdatableCredential
 * @see UcpDataSourceConfig
 * @see PoolDataSource
 */
public class UcpCredentialsUpdater implements UpdatableCredential<String> {

  /** The Oracle UCP pool data source to manage credentials for. */
  private final PoolDataSource poolDataSource;

  /** Supplies the UCP pool manager. Replaced in tests. */
  private final ManagerSource managers;

  /**
   * Creates a new credentials updater for the given Oracle UCP pool.
   *
   * @param poolDataSource the Oracle UCP PoolDataSource to manage
   */
  public UcpCredentialsUpdater(final PoolDataSource poolDataSource) {
    this(poolDataSource, UniversalConnectionPoolManagerImpl::getUniversalConnectionPoolManager);
  }

  /**
   * Creates an updater that gets the pool manager from the given source.
   *
   * @param poolDataSource the Oracle UCP PoolDataSource to manage
   * @param managers supplies the UCP pool manager
   */
  UcpCredentialsUpdater(final PoolDataSource poolDataSource, final ManagerSource managers) {
    this.poolDataSource = poolDataSource;
    this.managers = managers;
  }

  /**
   * Updates the credentials used by the Oracle UCP pool and refreshes connections if the pool
   * exists.
   *
   * <p>This method atomically updates the username and password on the pool, then refreshes the
   * pool if it is registered with the manager. The refresh operation gracefully replaces existing
   * connections with new ones using the updated credentials. Active connections continue to work
   * until they are returned to the pool.
   *
   * @param username the new database username
   * @param credential the new database password
   * @throws RuntimeException if the credentials cannot be updated or the pool cannot be refreshed
   */
  @Override
  public void setCredential(final String username, final String credential) {
    final String poolName = this.poolDataSource.getConnectionPoolName();
    try {
      synchronized (this.poolDataSource) {
        this.poolDataSource.setUser(username);
        this.poolDataSource.setPassword(credential);
      }
      final UniversalConnectionPoolManager mgr = managers.get();
      // A pool is registered with the manager only once it is created. Until then the new
      // credentials
      // are already on the PoolDataSource, so there is nothing to refresh.
      if (Arrays.asList(mgr.getConnectionPoolNames()).contains(poolName)) {
        mgr.refreshConnectionPool(poolName);
      }
    } catch (final SQLException e) {
      throw new CredentialRotationException(
          "Failed to update credentials in poolDataSource " + poolName, e);
    } catch (final UniversalConnectionPoolException e) {
      throw new CredentialRotationException("Failed to refresh poolDataSource " + poolName, e);
    }
  }

  /** Source of the UCP pool manager, so tests can substitute a mock. */
  @FunctionalInterface
  interface ManagerSource {
    /**
     * Returns the UCP pool manager.
     *
     * @return the pool manager
     * @throws UniversalConnectionPoolException if the manager cannot be obtained
     */
    UniversalConnectionPoolManager get() throws UniversalConnectionPoolException;
  }
}