NAME

DBIO::AccessBroker - Credential lifecycle for DBIO connections

VERSION

version 0.900004

SYNOPSIS

# Static — one fixed set of connection parameters
use DBIO::AccessBroker::Static;
my $broker = DBIO::AccessBroker::Static->new(
    dbname => 'myapp',
    username => 'app', password => 'secret',
);
# Storage gets storage-native connect info
my $info = $broker->current_connect_info_for_storage($schema->storage);

# Vault — rotating credentials from OpenBao/Vault
use DBIO::AccessBroker::Vault;
my $broker = DBIO::AccessBroker::Vault->new(
    vault     => WWW::OpenBao->new(endpoint => 'http://vault:8200', token => $token),
    dsn       => 'dbi:Pg:dbname=myapp;host=db',
    cred_path => 'database/creds/myapp',
    ttl       => 3600,         # credentials valid for 1 hour
    refresh_margin => 900,     # refresh 15 min before expiry
);
# DBIO can now connect directly with a broker
my $schema = MyApp::Schema->connect($broker);

# the async mode is the connect call's, beside the broker
my $async_schema = MyApp::Schema->connect($broker, { async => 'future_io' });

# so are the other connect options -- the broker answers which database,
# and as whom (see DBIO::Storage::DBI/connect_info)
my $schema = MyApp::Schema->connect($broker, {
    quote_names   => 0,
    on_connect_do => [ 'SET search_path TO app' ],
});

See t/access_broker/ for a runnable example.

DESCRIPTION

AccessBroker is a CredentialSource: it supplies the connect info for exactly one backend identity (one set of credentials). It is storage-agnostic — it returns connection parameters, not handles — so it works with both Storage::DBI (sync) and Storage::Async (async/Future-based). It handles:

  • Credential lifecycle — fetching, rotating, and caching database credentials

A broker does not route, and it does not own a host list. Read/write routing and the master/replicant topology belong to DBIO::Replicated. One credential can serve many servers via a "for_host" view, which pairs this single identity with one host at connect time.

METHODS

set_storage

$broker->set_storage($storage);

Attaches this broker to a storage instance, so that a subsequent "current_connect_info_for" knows which storage to hand to connect_info_for_storage. ("current_connect_info_for_storage" is given its storage as an argument and does not read this one.) Called by Storage when the broker is attached; not normally called directly.

A broker keeps a reference to every storage it is attached to: a synchronous owner and its embedded async backend, or the master and the replicants of DBIO::Replicated, are several storages of one broker. Setting a storage adds it; storages already attached stay. Setting the same storage again changes nothing.

The storages are held weakly: the storage holds its broker, not the other way round. Held strongly, the two would keep each other alive, and a storage dropped with a broker attached would never close its connection. A storage that is gone is no longer attached to the broker.

unset_storage

$broker->unset_storage($storage);

Detaches this broker from exactly this storage; the other storages it is attached to are left as they are. Called by Storage when it detaches the broker ("clear_access_broker" in DBIO::Storage) or is given another ("set_access_broker" in DBIO::Storage), before its broker slot changes. A storage the broker is not attached to is ignored. Returns the storage.

connect_info_for

my $info = $broker->connect_info_for;

Optional legacy DBI-shaped connect info. The base implementation throws; built-in brokers implement it, and the default "connect_info_for_storage" delegates to it.

connect_info_for_storage

my $info = $broker->connect_info_for_storage($storage);

Returns storage-native connect info -- the primary interface subclasses implement. The default delegates to "connect_info_for"; brokers with a storage-native format override it.

A storage takes two shapes of answer:

Anything else that is something -- a plain string (a bare DSN), a code reference, an object -- is refused by the sync and the async storage alike, with an error naming the broker class and the shape of the answer, never a value of it. An answer of nothing -- undef, an empty hash or array, a false value -- is refused too, naming the broker ("current_access_broker_connect_info" in DBIO::Storage); a storage never falls back to an earlier answer.

An answer that names no database is refused by the sync storage as well: the DBI form or a hash with an undefined or empty DSN, and a hash with neither a dsn nor a dbname key -- credentials alone, or host and port, which are never passed on as DBI attributes. The error names the broker class and the shape of the answer, and no connection is opened: the credentials are for the database the broker names, so a storage with a broker attached never connects to $ENV{DBI_DSN} instead. Answer a dsn or a dbname.

A DBIO::Storage::DBI connects with a hash answer only when it applies all of it. Beside the connection itself that is: the connect options of "connect_info" in DBIO::Storage::DBI and DBI attributes -- a name starting with an uppercase letter, with private_, dbi_ or dbd_, or with the prefix of the DBD the DSN names (pg_ for dbi:Pg:). Any other key set to a value is a parameter of the database client -- sslmode, connect_timeout, sslrootcert, ... -- which an async transport hands to its client, while the sync storage builds only dbname, host and port into the DSN and DBI->connect would silently not apply the rest. The sync storage never opens a connection without such a parameter: when it is to connect with the answer it throws instead, the error naming the broker class and the keys.

That is when the storage opens a connection of its own -- the first use of its dbh, and every reconnect -- not when it is configured. A schema connected with an async mode whose transport takes the hash is configured with such an answer, and its pooled connections are opened with the parameter; only the owning storage's own connection throws, once something asks for it. There are two ways to give the parameter to both. Answer the DBI form, with it inside the DSN:

[ 'dbi:Pg:dbname=myapp;host=db;sslmode=require', $user, $password, \%attrs ]

The attribute hash of the DBI form is not looked into, so a DBD attribute the storage does not recognise as one goes there. Or answer by the $storage that asks -- the transport the hash with its client's parameters, a DBIO::Storage::DBI an answer it applies in full:

sub connect_info_for_storage {
  my ($self, $storage, $mode) = @_;
  return $storage->isa('DBIO::Storage::DBI')
    ? [ 'dbi:Pg:dbname=myapp;host=db;sslmode=require', $user, $password ]
    : { dbname => 'myapp', host => 'db', user => $user, password => $password,
        sslmode => 'require' };
}

The same holds for a key given beside the broker, $schema->connect($broker, { sslmode => 'require' }): it joins the DBI attributes of every answer, so the sync storage throws when it is to connect, naming the keys as standing beside the broker. A parameter of the database client is the broker's to answer, in one of the two ways above.

An answer in one of the two shapes that the sync storage still cannot connect with -- a libpq keyword/value string where the DSN belongs, as an ev transport takes it -- is not refused here: DBI->connect fails on it, and the error is DBI's. No message about a connection quotes the password, though: in that error, as everywhere DBIO writes connect info into a message, it is shown as *** ("connect_info" in DBIO::Storage::DBI).

needs_refresh

Returns true if credentials should be rotated. Default: false.

refresh

Performs credential rotation. Default: does nothing.

has_rotating_credentials

Returns true if credentials rotate across connections. Default: false.

is_transaction_safe

Returns true if DBIO may open transactions through this broker. Default: true unless "has_rotating_credentials"; see "TRANSACTION SAFETY".

current_connect_info_for

my $info = $broker->current_connect_info_for($mode);
my $info = $broker->current_connect_info_for($mode, $storage);

Legacy DBI-shaped convenience: calls refresh first if needs_refresh is true, then returns $self->connect_info_for_storage($storage, $mode).

Given $storage, it answers for that storage, whether or not the broker is attached to it. Without it the broker answers for the storage it is attached to ("set_storage"): $self->connect_info_for($mode) when it is attached to none, and the answer for that storage when it is attached to one. A broker attached to several storages cannot tell which one is meant, and throws -- naming its class and the count, no credential -- before it refreshes anything. Pass the storage, or use "current_connect_info_for_storage".

for_host

my $view = $broker->for_host('db1.example.com');
my $view = $broker->for_host({ host => 'db1.example.com', port => 5433 });

Pairs this single credential identity with one host, returning a DBIO::AccessBroker::HostBound view. The view shares this broker's credentials and rotation lifecycle but reports the given host in its connect info, so one credential can serve many servers without this broker ever knowing the host list. Accepts a plain host string or a hashref.

The application builds the views and hands each to the connection on that host -- the master's connect, a replicant's entry in "connect_replicants" in DBIO::Replicated::Storage as [ $view ]. DBIO::Replicated passes a view through like any broker; it builds none.

current_connect_info_for_storage

my $info = $broker->current_connect_info_for_storage($storage, $mode);

Storage-aware convenience: calls refresh first if needs_refresh is true, then returns $self->connect_info_for_storage($storage, $mode).

TRANSACTION SAFETY

A broker only supplies credentials, so the sole hazard to a running transaction is credentials rotating mid-flight. DBIO distinguishes:

  • has_rotating_credentials() — new connections may need refreshed credentials

  • is_transaction_safe() — DBIO may start a transaction through this broker without an explicit override

The default implementation treats brokers as transaction-safe unless they rotate credentials.

This means:

Starting a transaction through a broker marked as unsafe will throw by default. If you intentionally want to allow this, set DBIO_ALLOW_UNSAFE_BROKER_TRANSACTIONS=1. DBIO will then proceed, but emit a warning on transaction start.

The check is made where a transaction starts. The synchronous "txn_begin" in DBIO::Storage throws, and with it txn_do and txn_scope_guard. txn_do_async does not throw: in every async mode it returns a failed Future carrying the same refusal, and on an async backend ("txn_do_async" in DBIO::Storage::Async) no connection is taken from the pool for it. "deploy_async" in DBIO::Storage::Async is refused the same way where it runs its DDL in a transaction. "pipeline" in DBIO::Storage::Async is refused the same way, as a failed Future before a connection is taken: a batch is work meant to stand as a unit, and on PostgreSQL it is an implicit transaction.

SUBCLASSING

Implement these methods, each documented under "METHODS": "connect_info_for_storage", "connect_info_for" (optional, legacy), "needs_refresh", "refresh", "has_rotating_credentials" and "is_transaction_safe".

The $mode argument

The broker methods accept a trailing $mode argument ('read' or 'write') for backward compatibility. It is vestigial: a broker is a single-identity CredentialSource with nothing to route on, so all built-in brokers ignore it. Routing decides read versus write — see DBIO::Replicated — not the broker. New broker subclasses should not branch on $mode.

AUTHOR

DBIO & DBIx::Class Authors

COPYRIGHT AND LICENSE

Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.