Skip to content

pgdog-plugin: add the authenticate hook - #1572

Open
ziomarco wants to merge 2 commits into
pgdogdev:mainfrom
ziomarco:split/plugin-abi-auth-hook
Open

ziomarco wants to merge 2 commits into
pgdogdev:mainfrom
ziomarco:split/plugin-abi-auth-hook

Conversation

@ziomarco

Copy link
Copy Markdown
Contributor

This adds a client authentication hook to the pgdog-plugin ABI. It is the first PR of the auth plugin work discussed in the RFC (https://gist.github.com/ziomarco/ad18e5c5f65472f13671958514b30c0d) and it only contains the ABI and loader groundwork. The auth_type = "plugin" host wiring, the per-user server_role and the Google plugin come in follow-up PRs.

A plugin implements Plugin::authenticate(AuthContext) -> AuthDecision. The context carries the startup packet user and database, the credential the client presented, the client address, the TLS identity and whether TLS is in use, all as borrowed PdStr values valid for the call. The decision is Skip (not my credential, ask the next plugin), Allow(AuthGrant) or Deny(reason). The grant can name a derived user, a server_role to assume on the backend, backend credentials, a read-only flag and a provision flag. The default implementation returns Skip, so every existing plugin keeps compiling and behaving as before.

Nothing owned crosses the FFI boundary. The plugin keeps its decision on its own stack and the generated bridge streams each string to the host through a sink callback, so there is no allocator handshake and nothing to free on either side. A panic inside authenticate is caught on the plugin side and turned into a Deny rather than unwinding into PgDog.

Because this appends a slot to PluginVtable, pgdog-plugin goes from 0.4.0 to 0.5.0 and plugins built against 0.4 are skipped at load time with a warning, as the existing major.minor gate already does. The in-tree plugins and the integration test plugins build unchanged.

The hook documents its own contract: PgDog calls it from the blocking pool, so it may run concurrently on several threads, blocking I/O is expected and wants its own timeout, and a panic becomes a Deny. The host callback the bridge passes in must not panic, which is documented where it is implemented. AuthOutcome::read_only_flag() decodes the POD read-only code (with named constants instead of 0/1/2 literals) so the host does not re-implement the match, and unknown codes decode to "unset".

On the PgDog side the loader now keeps plugins in [[plugins]] order (an IndexMap), so "first plugin to answer wins" is deterministic. It also keeps one slot per configured plugin instead of compacting the library vector, which used to pair the wrong library with a plugin name whenever an earlier plugin failed to dlopen.

🤖 Generated with Claude Code

ziomarco and others added 2 commits September 16, 2026 18:26
Plugins can now take part in client authentication. A plugin implements
Plugin::authenticate, receives the user, database, credential, client
address and TLS details as borrowed strings, and returns Skip, Allow or
Deny. Allow can carry a derived user, a server_role to assume on the
backend, backend credentials, a read-only flag and a provision flag.

No ownership crosses the FFI boundary: the owned decision stays on the
plugin's stack and each string field is streamed to the host through a
sink callback as a borrowed PdStr while the call is in progress, so
there is nothing to free on either side. A panic inside the plugin's
authenticate is caught on the plugin side and turned into a Deny, so it
never unwinds into the host as a foreign exception.

The hook is a new slot appended to PluginVtable, which is an ABI change,
so pgdog-plugin moves from 0.4.0 to 0.5.0. The loader compares
major.minor, so plugins built against 0.4 are skipped with a warning
rather than loaded against a vtable they do not match.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Store loaded plugins in an IndexMap so they are consulted in the order
they appear in pgdog.toml. HashMap iteration order was arbitrary, which
made "first plugin to answer wins" nondeterministic for router plugins
and would make ordering authentication plugins impossible.

Keep one Option<Library> slot per configured plugin instead of dropping
failed dlopen results from the vector. Previously a plugin that failed
to load shifted every later library down one index, so the next plugin
name was paired with the wrong library. Also drop the unwrap on LIBS
right after setting it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant