Repository navigation
fix: searches read every grant to the tenant's members and the public on the listed types even for the anonymous and guests, read each grant's row, and as of a time read every version of grants they don't count or of other types, and checks every version of their subject's grants - #82
Merged
Merged
Conversation
…ant's members and the public on the listed types, of built-in roles too, even for the anonymous and guests, who count none of the members'
…their subject holds on the listed types, as they tell the public's and the members' grants by a kind no index holds
…bers' grants on the listed types for the anonymous and guests, and each version of a group's grants on other types once for each type, and checks read every version of their subject's grants
…uilds the grants' index and writes history before a grant, and the README names it 0019 The migration now locks rbac_grants and then rbac_history before it builds anything, as 0013 does, so a change that starts while it builds waits for it. The README says a transaction under way that changed a table, not only one that read history, deadlocks with it.
# Conflicts: # README.md
…le it builds the grants' index, as Migrate now gives way to a lock it waits for Since Migrate waits at most 100 ms for a lock, a change holding history while it waits for the grants made it give way once it had built the index, rather than deadlock. The migration's notes say so.
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.
Summary
Every Filter, Reachable and Holders reads every grant to the tenant's members and the public on the listed types, of built-in roles too, even for the anonymous and guests, who count none of the members'. (L1, and the first half of X2; a regression from fix: searches read every grant of a custom role in the tenant once planned generically, and as of a time lists read every version of every deny on their types and searches and Grantable every version of every custom role #76)
The problem: before its statement, a search reads which custom roles the broad principals' grants name on the listed types or path (
broadArgs). fix: searches read every grant of a custom role in the tenant once planned generically, and as of a time lists read every version of every deny on their types and searches and Grantable every version of every custom role #76 moved therole LIKE 'custom.%'test outside that read (namedCustom) so a generic plan couldn't pickrbac_grants_custom_roleby the tenant alone. As a result the read went throughrbac_grants_principal, which holds every grant of the broad principals, built-in roles' too, and it read both broad keys whatever the subject counts. Withmembers:t1holding built-incluster.loginon 20,000 clusters, the anonymous subject'sFilterwent from 33 to 4,713 pages ofrbac_grants(17 → 28 at 05abbfb), and a member'sReachablefrom 30 to 9,378. The same happened in tenants with no custom role at all, and for a permission no public role gives, the public's grants were read for nothing.The fix: migration
0018_principal_seeksaddsrbac_grants_custom_principal, a partial index on(principal, tenant, scope) INCLUDE (role) WHERE role LIKE 'custom.%'. Live, the prefix is tested inside the per-key, per-type read again. The new index leads with the principal, the tenant and the scope, so it is more selective thanrbac_grants_custom_roleunder any plan, and it holds no built-in role's grant. fix: searches read every grant of a custom role in the tenant once planned generically, and as of a time lists read every version of every deny on their types and searches and Grantable every version of every custom role #76's two tests still pass.broadArgsalso reads only the broad keys its caller counts:pthrough, so the public's only where a public role can give it.keysSQLcounts it. Filter's doc now says the condition also fixes that.The index or the key filter alone isn't enough: when the members hold a built-in role on thousands of clusters and others hold a custom role on thousands, either old index reads all of one or the other. A guest or the anonymous beside the members' custom grants reads them all unless the members' key is dropped.
Filter, its condition and Reachable read the row of every grant their subject holds on the listed types. (X4)
The problem:
filterHeld,filterCountedand Reachable's held read selected and testedprincipal_kind. Norbac_grantsindex holds it, so every grant they read was fetched from the heap: 2,064 heap pages for a subject holding a grant on each of 10,000 clusters, against 1 forAuthorize.The fix: the broad principals are told apart by their key (
narrowKey: notpublic:, notmembers:%). That is equivalent, because a kind holds no colon, the public's id is empty, and the tables check both.principal_kindis dropped from the columns read, so the read is index-only onrbac_grants_principal.broadKindsSQLis gone.As of a time, Filter and Reachable read every version of the members' grants on the listed types for the anonymous and guests, and each version of a group's grants on other types once for each type. (X2's second half, and the as-of read of a subject's own grants)
The problem: history's index of a principal's versions,
rbac_history_principal (tenant, principal, entity, effective_at), holds no scope. Each per-type read of a principal's grants (the subject's in Filter's and Reachable's held reads, the broad principals' inbroadArgs) either:rbac_history_scope, everyone's.Which one depended on statistics. With L1's fix in, the auditor's anonymous
Filteras of a time beside the members' grants on clusters still went from 107 to 2,978 history pages. Beside a guest's group holding a role on thousands of volumes, a list of clusters went 382 → 6,363.The fix: the same migration replaces
rbac_history_principalwith an index on(tenant, principal, entity, split_part(scope, ':', 1), scope, effective_at DESC) WHERE principal IS NOT NULLand analyzes history. As of a time,byTypetests a type assplit_part(g.scope, ':', 1) = t.t, an equality this index seeks, instead of a range of scopes.unnest(types)gets a default estimate of a few rows. In a history of few principals, where the plan estimates a principal's versions as a large share, that maderbac_history_scopelook as cheap even with the principal as a constant (cost 52 vs 95 for the same one-row estimate). The equality is onerbac_history_scopecan't serve, and ANALYZE keeps statistics for the expression.As of a time, a check, Permissions and Grantable read every version of their subject's grants to find those on the resource's path. (found while measuring the old index's readers)
The problem: the same index held no scope, so a plan could read a principal's grant versions only all at once. In a history of many tenants,
Authorize,PermissionsandGrantablefor a subject holding 3,000 grants read 965, 965 and 994 history pages.The fix: the replacement index holds the scope after its type, and PostgreSQL 18's skip scan seeks it over the few types, with no change to the checks' statements: 68, 68 and 100 pages. Every other as-of reader read the same pages within ±3: Explain, Grants, Eligibilities, Memberships, GroupsOf, Members, Holders.
The migration is numbered 0018, after #79's
0017_transaction_dates, which is on canary and merged into this branch. It builds an index onrbac_grants, which holds writes to it, and drops and rebuilds history's index, which holds reads of history and every change until it commits. It locksrbac_grantsand thenrbac_historybefore it builds either, as 0013 does: locked as each statement reached it, history was locked only once the grants' index was built, so a change that started during the build and wrote history before a grant, asDeleteScopedoes (a scope's eligibilities, then its grants), madeMigrategive way after building the index (and a host's own tool without alock_timeoutdeadlock with it). Now only a transaction already under way when it starts, that read history or made a change and then writes a grant, can make it give way, before it builds anything. The README's upgrade notes say this.Testing
RBAC_TEST_DATABASE_URL=… GODEBUG=fips140=only. Each test was run with its fix reverted in a form that compiles:TestSearchesReadOnlyTheBroadGrantsTheyCount(live under custom and generic plans, as of a time under AsOf's plans, in the crowded harness): grants on 500 → 5,000 clusters to the members, to others, and to the public, with the subjects that don't count them.Filter154 → 1,183 and 166 → 1,187.FilterandReachableabout 110–150 → 1,125; as of a time about 2,230 → 22,060; a member's search ofcluster:rootbeside the public's custom grants about 113–156 → 1,136.Filter109 → 1,122 and 145 → 1,134.TestListingsReadTheirGrantsFromAnIndexAlone(custom and generic plans, afterVACUUMmarks every row visible; skips otherwise): a subject's grants on 1,000 clusters among others'. Without the fix,Filterwith its condition andReachableeach read about one heap page per grant (2,972 and 2,963 at 3,000 grants); the bound is 10.TestChecksReadTheirGrantsFromAnIndexAlonenow shares its VACUUM loop (grantsVisibleToAll).TestAsOfSearchesSeekTheirPrincipalsGrantsOnEachType(one tenant and many, as AsOf plans and generically): the members' grants on 500 → 5,000 clusters and the guest's group's on as many volumes, searched by the anonymous and the guest, plus Holders.Filter748 → 5,979, the guest'sFilter382 → 6,363 andReachable125 → 1,523; all four cases fail.Filter605 → 4,526), and many tenants fails generically.Filter1,423 → 12,240.TestAsOfChecksSeekTheirSubjectsGrantsOnThePath(crowded; AsOf's and generic plans): a subject's grants on 500 → 5,000 other clusters. With the type-only index,Authorize80 → 537 (Grantable94 → 551); with the old index, 77 → 534.TestMigratingThePrincipalSeeksWaitsForAChangeStartedAsItBuilds:Migrate, tried once, whileDeleteScopestarts during the grants' index build (100,000 grants,maintenance_work_mem=64kB). With the tables locked as each statement reached them,Migratefails with 55P03 after building the index, 3 runs of 3; applied without alock_timeout,DeleteScopefails as a deadlock.TestSearchesUnderAGenericPlanReadOnlyTheBroadGrantsOfCustomRoles,TestSearchesReadAsMuchOnceAConnectionPlansThemGenerically, everyTestAsOf*, and the rest of the suite pass, with-race, after merging canary.TestThePackageRunsAsTheReadmesDatabaseRoles(from fix: as of a time, a revoke a procedure or DO block makes after it commits counts from before moments it came after, migrating deadlocks with a change reading the tables in another order, the README's role setup fails every call, and AsOf panics on a nil read #79) applies 0018 withMigrateas the README's administrator's role once the roles are hardened.Filterreads 28 → 40 pages live (36 → 28 generic). As of a time beside 10,000 it reads 72 → 92 history pages, and the guest'sReachable77 → 107. The listing query's heap reads are 1 page, against 2,064 before.Filter,Reachable,HoldersandGrantablewithAuthorizeandApply, live and as of one or several times: all seven tests pass at 12 seeds.Grantsof a principal 1,240 → 1,483), as the time is no longer the index's range; in many tenantsAuthorize,PermissionsandGrantableread less (106 → 48).Authorizethen refuses. One built in those states and used once it is a member keeps the members' grants of built-in roles but not of custom ones, as Filter's doc says.user:members:t1,user:public:,group:public:, declared kindsmembersx,public_,members-), and as of a time scope ids holding colons (cluster:a:1,workspace:cluster:a) beside types that start alike (org,organization):FilterandReachableagree withAuthorize.Filtercall reads every version of the members' grants of built-in roles on the type (2,254 → 21,590), as the prefix isn't tested there; its query counts them anyway (4,510 → 43,181).:,'%_,members:t1,group:xand spaces, as of a time too; round 7's at 16 fresh seeds. All pass butTestAuditPlainListingsAsOfATime'sMembershipsOf, which reads every tenant now and only the one as of a time, asAsOfdocuments.Checklist
make checkpassesrbac_history_principalkeeps its name with new columns, and the migration is new and unreleased.