@everystack/cli 0.4.72 → 0.4.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/cli/derived-grants.ts +20 -13
- package/src/cli/derived-plan.ts +15 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to `@everystack/cli` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.73] - 2026-09-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- db:reconcile converges an owned relation's self-grant
|
|
12
|
+
|
|
7
13
|
## [0.4.72] - 2026-09-04
|
|
8
14
|
|
|
9
15
|
### Fixed
|
package/package.json
CHANGED
|
@@ -106,24 +106,30 @@ export interface ObjectGrantDiff {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Ownership context for
|
|
109
|
+
* Ownership context for an object's grant diff — passed for EVERY kind (functions AND relations).
|
|
110
110
|
*
|
|
111
|
-
* Owner-implicit
|
|
112
|
-
* correct in isolation — an owner's privileges
|
|
113
|
-
* non-owner grants is computed RELATIVE TO THE OWNER, so
|
|
114
|
-
* produce two different sets for the same
|
|
115
|
-
* that is purely ownership.
|
|
111
|
+
* Owner-implicit privileges are excluded from both grant reads (`grantee <> proowner` for
|
|
112
|
+
* functions, `<> relowner` for relations), which is correct in isolation — an owner's privileges
|
|
113
|
+
* are not grants. But it means the SET of non-owner grants is computed RELATIVE TO THE OWNER, so
|
|
114
|
+
* two sides with different owners produce two different sets for the same object, and the differ
|
|
115
|
+
* reconciles a difference that is purely ownership.
|
|
116
116
|
*
|
|
117
|
-
*
|
|
117
|
+
* First measured on functions, 2026-08-10: a consumer's local `resolve_user_slug` is owned by
|
|
118
118
|
* `outbound_migrator` (owner-implicit, so the pull rendered NO privilege), while on stage the
|
|
119
119
|
* same function is owned by `slug_resolver` and `outbound_migrator` holds an EXPLICIT grant.
|
|
120
120
|
* The differ planned `REVOKE EXECUTE ON api.resolve_user_slug FROM outbound_migrator` — a real
|
|
121
121
|
* access removal minted from an ownership difference. They did not apply it.
|
|
122
|
+
*
|
|
123
|
+
* The relation half surfaced 2026-09-04: after `db:provision ADOPT` made `outbound_migrator` own a
|
|
124
|
+
* VIEW the model also lists in its grants, the live read excluded the owner's own rows, so the
|
|
125
|
+
* declared self-grant was minted on every reconcile and never converged. The relation call site
|
|
126
|
+
* used to pass no ownership context on the belief that a relation's grant set does not shift with
|
|
127
|
+
* its owner — true only while relacl is NULL. Both kinds now share this context.
|
|
122
128
|
*/
|
|
123
129
|
export interface GrantOwnerContext {
|
|
124
130
|
/** The owner the DECLARED grant set was computed against, when the models declare one. */
|
|
125
131
|
declared?: string;
|
|
126
|
-
/** The owner the LIVE grant set was computed against (`pg_get_userbyid(proowner)`). */
|
|
132
|
+
/** The owner the LIVE grant set was computed against (`pg_get_userbyid(proowner|relowner)`). */
|
|
127
133
|
live?: string;
|
|
128
134
|
}
|
|
129
135
|
|
|
@@ -131,8 +137,9 @@ export interface GrantOwnerContext {
|
|
|
131
137
|
* The idempotent REVOKE/GRANT delta between the declared contract and the live ACLs —
|
|
132
138
|
* the table path's reconcileGrants, per derived object. Empty when they match.
|
|
133
139
|
*
|
|
134
|
-
* When `owners` is supplied (functions) and the two sides cannot be
|
|
135
|
-
* computed against the SAME owner, REVOKEs are WITHHELD and reported. GRANTs
|
|
140
|
+
* When `owners` is supplied (every kind — functions and relations) and the two sides cannot be
|
|
141
|
+
* shown to have been computed against the SAME owner, REVOKEs are WITHHELD and reported. GRANTs
|
|
142
|
+
* still flow: a
|
|
136
143
|
* redundant GRANT is noise, a wrong REVOKE removes access that was really there. Failing to
|
|
137
144
|
* revoke is the recoverable error and it is never silent.
|
|
138
145
|
*
|
|
@@ -198,10 +205,10 @@ export function diffObjectGrants(
|
|
|
198
205
|
out.push(`REVOKE ${toRevoke.join(', ')} ON ${target} FROM ${grantee}`);
|
|
199
206
|
} else {
|
|
200
207
|
const why = grantee === owners!.live
|
|
201
|
-
? `'${grantee}' is the
|
|
202
|
-
: `'${grantee}' is the
|
|
208
|
+
? `'${grantee}' is the object's live OWNER, whose implicit privileges are excluded from both grant reads — so this difference is ownership, not a grant`
|
|
209
|
+
: `'${grantee}' is the object's declared owner (live owner: ${owners!.live}), and an owner's implicit privileges are excluded from both grant reads`;
|
|
203
210
|
suppressed.push(
|
|
204
|
-
`${target}: withheld REVOKE ${toRevoke.join(', ')} FROM ${grantee} — ${why}. An owner's implicit privileges are excluded from both reads, so the two grant sets are not comparable and this revoke may remove access that is really there. Declare the
|
|
211
|
+
`${target}: withheld REVOKE ${toRevoke.join(', ')} FROM ${grantee} — ${why}. An owner's implicit privileges are excluded from both reads, so the two grant sets are not comparable and this revoke may remove access that is really there. Declare the object's owner to restore revokes.`,
|
|
205
212
|
);
|
|
206
213
|
}
|
|
207
214
|
}
|
package/src/cli/derived-plan.ts
CHANGED
|
@@ -482,11 +482,21 @@ export function planReconcile(
|
|
|
482
482
|
);
|
|
483
483
|
} else {
|
|
484
484
|
const target = parsed.target ?? (src.kind === 'function' ? `FUNCTION ${src.identity}` : src.identity);
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
485
|
+
// Ownership context is passed for EVERY kind. An owner's implicit privileges are excluded
|
|
486
|
+
// from the live read for relations too (`a.grantee <> c.relowner`), so a model that both
|
|
487
|
+
// owns an object and lists that owner in its grants declares a self-grant the read can
|
|
488
|
+
// never confirm — diffObjectGrants normalizes the owner grantee away. The earlier belief
|
|
489
|
+
// that a relation's grant set "does not shift with its owner" holds only while relacl is
|
|
490
|
+
// NULL; the first non-owner grant materializes the ACL and the owner's own rows drop out,
|
|
491
|
+
// exactly as for a function. (A consumer's ADOPT'd view, owned by a role its model also
|
|
492
|
+
// grants, looped forever — 2026-09-04.)
|
|
493
|
+
// KNOWN LIMIT: a privilege an owner has deliberately self-revoked is not restored — that
|
|
494
|
+
// is a failure to ADD access (availability), never to remove it, and unnormalized code did
|
|
495
|
+
// not converge on it either.
|
|
496
|
+
const owners = {
|
|
497
|
+
...(src.owner !== undefined ? { declared: src.owner } : {}),
|
|
498
|
+
...(liveObj.owner !== undefined ? { live: liveObj.owner } : {}),
|
|
499
|
+
};
|
|
490
500
|
const { statements, suppressed } = diffObjectGrants(target, parsed.grants, liveObj.grants, owners);
|
|
491
501
|
if (statements.length > 0) regrants.push({ identity: src.identity, statements });
|
|
492
502
|
// Never silent: a withheld revoke is reported every run until the owner is declared.
|