Repository navigation
Install without superuser; delete tuples in bulk - #9
Merged
Merged
Conversation
Five functions declared SET fga._dispatching = 'on' to keep a compiled relation's hand-off to the generic resolver from dispatching straight back to the generated function. PostgreSQL lets only a superuser store a setting for a custom parameter in a function definition, and only a superuser can GRANT SET ON PARAMETER for one, so the installer stopped at the first of them for every other role. That includes the master user of a managed service such as Amazon RDS, which the install story promises to serve; it went unseen because the compose role is a superuser. Dispatch is now an argument instead of session state: fga.check and the list_objects search take it through internal fga._check and fga._list_objects, the public entry points pass true and the hand-offs pass false. Setting and restoring the flag in the body instead would need an EXCEPTION block, and so a subtransaction, on every hand-off. The wrapper adds no measurable cost to a check (about 210us per call either way, 20,000-call loops). The session escape hatch the flag also provided is gone; the tests that used it to force generic answers call the internal functions with dispatch off. A new test installs the bundle twice as a LOGIN CREATEDB CREATEROLE role into a database it owns, and then answers through a compiled relation, so a superuser-only statement fails the suite.
The installer of 0.1.1 needed superuser for one reason nobody looked for: a custom setting stored in a function definition. The list in decision 7 named the dependencies a managed service refuses, but not the privilege, and the compose role being a superuser hid it. Naming the trap, and the test that now catches the class, keeps the next contributor from reaching for a SET clause to carry state.
Revoking everything a subject holds, or everything on an object an application deletes, means a read followed by deletes of at most 100 full keys under upstream's API. In the database that holds the application's data it can be one DELETE ... RETURNING, and even a trigger on the application's own table, so a filtered delete earns a place in scope as an additive surface like compiled relations. Decision 8 kept every tuple mutation in one function so a changelog could be added later. Putting the filter into fga.write's request would keep that literally true but put a key upstream does not have into an upstream-shaped API, so the rule becomes one insert site and one delete site, which is what the changelog actually needs.
Taking every grant away from a user, or every tuple off an object being deleted, otherwise means reading the store and sending full keys back to fga.write 100 at a time, or selecting from fga.tuple and tying the caller to its layout. fga.delete_tuples does it in one DELETE ... RETURNING, so a concurrent revoke waits and then gets nothing back, and it returns the deleted rows, conditions included, because a caller auditing a revoke needs each key, not a count. The filter must name a whole subject or a whole object; anything less is refused, so no call can mean "the whole store", while an empty array matches nothing and so a trigger with no rows is harmless. Like fga.write's deletes it never consults the model, so rows a model change stranded stay deletable. The arguments and the result are typed rather than upstream-style strings: the surface is SQL-facing, like compiled relations, and its rows feed straight back into another call. fga.write's deletes now go through the same internal delete, the one site a changelog would hook. Building that statement per key doubled a 100-key delete (13 ms to 26 ms), so a filter naming every column takes a static, plan-cached branch: about 16 ms. Any other filter builds its statement from the filters given, so an index serves it, which a cached "($n IS NULL OR ...)" plan cannot do.
The release adds a public function and a public type, fga.delete_tuples and fga.deleted_tuple, so it is a minor version. It also makes the installer work for a role without superuser, such as a managed service's master user. The public entry points answer as before; the one thing gone is the undocumented session setting fga._dispatching, which forced generic answers. Re-running the installer upgrades in place.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two changes for 0.2.0.
Install without superuser. Five functions declared
SET fga._dispatching = 'on'. PostgreSQL lets only a superuserstore a custom setting in a function definition, so the installer
failed for every other role — including a managed service's master
user (Amazon RDS), which the install story promises to serve. The
compose role is a superuser, so the suite never saw it. Dispatch is
now an argument of internal
fga._check/fga._list_objects;public entry points pass true, the compiled hand-offs pass false.
No measurable cost on
fga.check(~210 µs either way).TestInstallWithoutSuperuserinstalls twice as aLOGIN CREATEDB CREATEROLErole and answers through a compiledrelation; it fails on
mainwithpermission denied to set parameter "fga._dispatching".fga.delete_tuples. Beyond upstream: delete every tuple matchinga filter anchored on a whole subject or a whole object, in one
DELETE … RETURNING, returning the deleted rows (typed,conditions included). Covers offboarding and resource deletion,
including from a trigger on the application's own table
(docs/INSTALL.md, "Deleting tuples in bulk"). An unanchored filter
is refused; empty arrays match nothing; the model is never
consulted, as with
fga.write's deletes.Decision 8 becomes "one insert site, one delete site":
fga.write's deletes run through the same internalfga._delete_tuples. A 100-keyfga.writedelete goes from ~13 msto ~16 ms (the exact-key filter takes a static, plan-cached
branch; building the statement per key cost ~26 ms).
CLAUDE.md changes land as their own commits (decision 7's
superuser trap; scope and decision 8).