@homeflare/alchemy 0.44.0 → 0.45.0
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/dist/postgres/fake-role-sql.d.ts +2 -1
- package/dist/postgres/fake-role-sql.d.ts.map +1 -1
- package/dist/postgres/fake-sql.d.ts +3 -3
- package/dist/postgres/fake-sql.d.ts.map +1 -1
- package/dist/postgres/index.d.ts +1 -1
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +117 -94
- package/dist/postgres/index.js.map +11 -11
- package/dist/postgres/role-attrs.d.ts +7 -4
- package/dist/postgres/role-attrs.d.ts.map +1 -1
- package/dist/postgres/role-diff.d.ts +2 -2
- package/dist/postgres/role-diff.d.ts.map +1 -1
- package/dist/postgres/role-errors.d.ts +13 -7
- package/dist/postgres/role-errors.d.ts.map +1 -1
- package/dist/postgres/role-membership-sql.d.ts +30 -27
- package/dist/postgres/role-membership-sql.d.ts.map +1 -1
- package/dist/postgres/role-provider.d.ts.map +1 -1
- package/dist/postgres/role-scram.d.ts +2 -0
- package/dist/postgres/role-scram.d.ts.map +1 -1
- package/dist/postgres/role-secrets.d.ts +8 -0
- package/dist/postgres/role-secrets.d.ts.map +1 -1
- package/dist/postgres/role-sql.d.ts.map +1 -1
- package/dist/postgres/role.d.ts +2 -2
- package/dist/postgres/role.d.ts.map +1 -1
- package/docs/postgres-role.md +18 -6
- package/package.json +1 -1
- package/src/ceph/ceph-argv.test.ts +1 -1
- package/src/launchd/sudo-allowlist.test.ts +2 -2
- package/src/linux/sudo-allowlist.test.ts +3 -3
- package/src/linux/sudo-runner.test.ts +2 -2
- package/src/postgres/fake-role-sql.ts +26 -6
- package/src/postgres/fake-sql.ts +10 -5
- package/src/postgres/index.ts +1 -0
- package/src/postgres/role-attrs.ts +7 -4
- package/src/postgres/role-diff.ts +7 -3
- package/src/postgres/role-errors.ts +22 -14
- package/src/postgres/role-guard.test.ts +3 -3
- package/src/postgres/role-identity.test.ts +14 -2
- package/src/postgres/role-lifecycle.test.ts +2 -2
- package/src/postgres/role-membership-options.test.ts +73 -0
- package/src/postgres/role-membership-repair.test.ts +153 -0
- package/src/postgres/role-membership-sql.ts +88 -63
- package/src/postgres/role-password-ascii.test.ts +53 -0
- package/src/postgres/role-password-transports.test.ts +123 -0
- package/src/postgres/role-privilege.test.ts +37 -10
- package/src/postgres/role-provenance.test.ts +1 -1
- package/src/postgres/role-provider.ts +3 -2
- package/src/postgres/role-refusals.test.ts +0 -25
- package/src/postgres/role-scram.ts +2 -0
- package/src/postgres/role-secrets.ts +15 -0
- package/src/postgres/role-sql.ts +3 -1
- package/src/postgres/role.ts +9 -1
- package/src/postgres/schema-socket.test.ts +1 -0
|
@@ -37,7 +37,8 @@ export interface PostgresRoleProps {
|
|
|
37
37
|
readonly login: boolean;
|
|
38
38
|
/** `CONNECTION LIMIT n`, `-1` for unlimited — maps to `pg_roles.rolconnlimit`. */
|
|
39
39
|
readonly connectionLimit: number;
|
|
40
|
-
/** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit
|
|
40
|
+
/** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit`; false also clears per-grant
|
|
41
|
+
* INHERIT on declared memberships (PG 16+ uses the role flag only to default new grants). */
|
|
41
42
|
readonly inherit: boolean;
|
|
42
43
|
/**
|
|
43
44
|
* `VALID UNTIL 'timestamp'` — maps to `pg_roles.rolvaliduntil`. ISO 8601 without a timezone is
|
|
@@ -86,18 +87,20 @@ export interface PostgresRoleAttributes {
|
|
|
86
87
|
*/
|
|
87
88
|
readonly memberOf: readonly string[] | null;
|
|
88
89
|
/**
|
|
89
|
-
* The membership rows behind `memberOf`, including `admin_option
|
|
90
|
+
* The membership rows behind `memberOf`, including `admin_option`, `set_option` and
|
|
91
|
+
* `inherit_option`. Present
|
|
90
92
|
* on a catalog read; stripped before attributes are stored (`storedAttributes`), because the
|
|
91
93
|
* engine's diff receives stored attributes and would never see them there. The provider's
|
|
92
94
|
* diff reads these rows live. `grantor` is the role that made the grant
|
|
93
|
-
* (`pg_auth_members.grantor`)
|
|
94
|
-
* be named in `REVOKE … GRANTED BY
|
|
95
|
+
* (`pg_auth_members.grantor`). Null is a defensive inconsistent-catalog case, not an ordinary
|
|
96
|
+
* DROP ROLE outcome; such a row cannot be named in `REVOKE … GRANTED BY`.
|
|
95
97
|
*/
|
|
96
98
|
readonly memberships?: readonly {
|
|
97
99
|
readonly parent: string;
|
|
98
100
|
readonly grantor: string | null;
|
|
99
101
|
readonly admin: boolean;
|
|
100
102
|
readonly set: boolean;
|
|
103
|
+
readonly inherit: boolean;
|
|
101
104
|
}[];
|
|
102
105
|
/**
|
|
103
106
|
* A seal of the last password value written, or `''` if none was ever written (`seal()` from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-attrs.d.ts","sourceRoot":"","sources":["../../src/postgres/role-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAExD,oEAAoE;AACpE,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAe/F,eAAO,MAAM,iBAAiB,UACrB,MAAM,KACZ;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAA;CAAE,GAAG,SAOrD,CAAC;AAEF;mEACmE;AACnE,eAAO,MAAM,cAAc,aAAc,MAAM,QAAQ,MAAM,GAAG,IAAI,KAAG,OACX,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC
|
|
1
|
+
{"version":3,"file":"role-attrs.d.ts","sourceRoot":"","sources":["../../src/postgres/role-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAExD,oEAAoE;AACpE,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAe/F,eAAO,MAAM,iBAAiB,UACrB,MAAM,KACZ;IAAE,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAA;CAAE,GAAG,SAOrD,CAAC;AAEF;mEACmE;AACnE,eAAO,MAAM,cAAc,aAAc,MAAM,QAAQ,MAAM,GAAG,IAAI,KAAG,OACX,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IAChC,gGAAgG;IAChG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;iGAC6F;IAC7F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,+FAA+F;AAC/F,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAC5C;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS;QAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;QACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAC3B,EAAE,CAAC;IACJ;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B"}
|
|
@@ -2,7 +2,7 @@ import type { Input } from 'alchemy/Input';
|
|
|
2
2
|
import * as Effect from 'effect/Effect';
|
|
3
3
|
import type { Environment } from '../secrets/write-only.ts';
|
|
4
4
|
import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
|
|
5
|
-
import { PostgresRoleIdentityRefused, type PostgresRoleNameRefused, PostgresRoleRenameRefused, type PostgresRoleValidUntilRefused } from './role-errors.ts';
|
|
5
|
+
import { PostgresRoleIdentityRefused, type PostgresRoleNameRefused, type PostgresRolePasswordNonAsciiRefused, PostgresRoleRenameRefused, type PostgresRoleValidUntilRefused } from './role-errors.ts';
|
|
6
6
|
/** A catalog read the provider performed for this diff. `found: undefined` means the name is
|
|
7
7
|
* absent. Omitting the argument means no catalog read (the offline comparison). */
|
|
8
8
|
export interface RoleObservation {
|
|
@@ -18,5 +18,5 @@ export declare const diffPostgresRole: (news: Input<PostgresRoleProps>, output:
|
|
|
18
18
|
readonly action: 'update';
|
|
19
19
|
} | {
|
|
20
20
|
readonly action: 'noop';
|
|
21
|
-
} | undefined, PostgresRoleRenameRefused | PostgresRoleNameRefused | PostgresRoleValidUntilRefused | PostgresRoleIdentityRefused>;
|
|
21
|
+
} | undefined, PostgresRoleRenameRefused | PostgresRoleNameRefused | PostgresRoleValidUntilRefused | PostgresRoleIdentityRefused | PostgresRolePasswordNonAsciiRefused>;
|
|
22
22
|
//# sourceMappingURL=role-diff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-diff.d.ts","sourceRoot":"","sources":["../../src/postgres/role-diff.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EACL,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,yBAAyB,EACzB,KAAK,6BAA6B,EACnC,MAAM,kBAAkB,CAAC;AAM1B;mFACmF;AACnF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,CAAC;CAC1E;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,SACrB,KAAK,CAAC,iBAAiB,CAAC,UACtB,sBAAsB,GAAG,SAAS,QACrC,WAAW,gBACF,eAAe,KAC5B,MAAM,CAAC,MAAM,CACd;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACrE,yBAAyB,GACzB,uBAAuB,GACvB,6BAA6B,GAC7B,2BAA2B,
|
|
1
|
+
{"version":3,"file":"role-diff.d.ts","sourceRoot":"","sources":["../../src/postgres/role-diff.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACjF,OAAO,EACL,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,mCAAmC,EACxC,yBAAyB,EACzB,KAAK,6BAA6B,EACnC,MAAM,kBAAkB,CAAC;AAM1B;mFACmF;AACnF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,CAAC;CAC1E;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,SACrB,KAAK,CAAC,iBAAiB,CAAC,UACtB,sBAAsB,GAAG,SAAS,QACrC,WAAW,gBACF,eAAe,KAC5B,MAAM,CAAC,MAAM,CACd;IAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACrE,yBAAyB,GACzB,uBAAuB,GACvB,6BAA6B,GAC7B,2BAA2B,GAC3B,mCAAmC,CA0CnC,CAAC"}
|
|
@@ -82,12 +82,9 @@ export declare class PostgresRoleCreateVanished extends PostgresRoleCreateVanish
|
|
|
82
82
|
declare const PostgresRoleMembershipUnrepaired_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
83
83
|
readonly _tag: "PostgresRoleMembershipUnrepaired";
|
|
84
84
|
} & Readonly<A>;
|
|
85
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* statement this family may issue can touch is a row whose grantor role was dropped
|
|
89
|
-
* (`pg_auth_members` keeps it keyed on the dead oid, and no `GRANTED BY` name reaches it), so
|
|
90
|
-
* the message asks the operator to revoke it by hand as a bootstrap superuser and re-plan. */
|
|
85
|
+
/** An option repair was refused, or a catalog row survived outside the declaration. A null grantor is a
|
|
86
|
+
* defensive corruption/test case: PostgreSQL tracks grantor dependencies, so ordinary
|
|
87
|
+
* DROP ROLE does not leave a dangling grantor. Refuse instead of claiming convergence. */
|
|
91
88
|
export declare class PostgresRoleMembershipUnrepaired extends PostgresRoleMembershipUnrepaired_base<{
|
|
92
89
|
readonly role: string;
|
|
93
90
|
readonly parent: string;
|
|
@@ -121,6 +118,15 @@ export declare class PostgresRoleIdentityRefused extends PostgresRoleIdentityRef
|
|
|
121
118
|
}> {
|
|
122
119
|
get message(): string;
|
|
123
120
|
}
|
|
124
|
-
|
|
121
|
+
declare const PostgresRolePasswordNonAsciiRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
122
|
+
readonly _tag: "PostgresRolePasswordNonAsciiRefused";
|
|
123
|
+
} & Readonly<A>;
|
|
124
|
+
/** Never carry the rejected password in this error: errors reach logs and traces. */
|
|
125
|
+
export declare class PostgresRolePasswordNonAsciiRefused extends PostgresRolePasswordNonAsciiRefused_base<{
|
|
126
|
+
readonly role: string;
|
|
127
|
+
}> {
|
|
128
|
+
get message(): string;
|
|
129
|
+
}
|
|
130
|
+
export type PostgresRoleError = PostgresRolePasswordNonAsciiRefused | PostgresRoleNameRefused | PostgresRoleRenameRefused | PostgresRoleValidUntilRefused | PostgresRolePasswordEnvUnsetError | PostgresRolePrivilegedRefused | PostgresRoleCreateVanished | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing | PostgresRoleIdentityRefused;
|
|
125
131
|
export {};
|
|
126
132
|
//# sourceMappingURL=role-errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-errors.d.ts","sourceRoot":"","sources":["../../src/postgres/role-errors.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"role-errors.d.ts","sourceRoot":"","sources":["../../src/postgres/role-errors.ts"],"names":[],"mappings":";;;AAYA;;;;;;;GAOG;AACH,qBAAa,uBAAwB,SAAQ,6BAA4C;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;;;iDAIiD;AACjD,qBAAa,yBAA0B,SAAQ,+BAA8C;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;;yDAGyD;AACzD,qBAAa,iCAAkC,SAAQ,uCAErD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;yFAEyF;AACzF,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;yEAEyE;AACzE,qBAAa,6BAA8B,SAAQ,mCAEjD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,WAAW,CAAC;CAC9C,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;uEAEuE;AACvE,qBAAa,0BAA2B,SAAQ,gCAA+C;IAC7F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAK7B;CACF;;;;AAED;;0FAE0F;AAC1F,qBAAa,gCAAiC,SAAQ,sCAEpD;IACA,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAY7B;CACF;;;;AAED;;sCAEsC;AACtC,qBAAa,yBAA0B,SAAQ,+BAA8C;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED;;mDAEmD;AACnD,qBAAa,2BAA4B,SAAQ,iCAAgD;IAC/F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAO7B;CACF;;;;AAED,qFAAqF;AACrF,qBAAa,mCAAoC,SAAQ,yCAEvD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;IAC1B,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;AAED,MAAM,MAAM,iBAAiB,GACzB,mCAAmC,GACnC,uBAAuB,GACvB,yBAAyB,GACzB,6BAA6B,GAC7B,iCAAiC,GACjC,6BAA6B,GAC7B,0BAA0B,GAC1B,gCAAgC,GAChC,yBAAyB,GACzB,2BAA2B,CAAC"}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* are exercised (`user-manag.sgml`). A safe seat membership is `ADMIN FALSE, SET FALSE`.
|
|
12
12
|
* ⛔ REVOKE BY GRANTOR. A plain `REVOKE parent FROM member` only touches rows the session's
|
|
13
13
|
* own grantor made; Postgres 18 warns and leaves another grantor's rows standing
|
|
14
|
-
* (`revoke.sgml`). Every revoke here names `GRANTED BY
|
|
15
|
-
*
|
|
14
|
+
* (`revoke.sgml`). Every revoke here names `GRANTED BY`. A mismatched grantor can still
|
|
15
|
+
* produce only a warning; the catalog re-read below catches surviving unsafe rows.
|
|
16
16
|
*/
|
|
17
17
|
import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
18
18
|
import * as Effect from 'effect/Effect';
|
|
@@ -20,30 +20,40 @@ import type { PgExecutor } from './database-sql.ts';
|
|
|
20
20
|
import { PostgresRoleMembershipUnrepaired, PostgresRoleParentMissing } from './role-errors.ts';
|
|
21
21
|
import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
|
|
22
22
|
/** One `pg_auth_members` row as this family reads it: the parent name, the grantor who made
|
|
23
|
-
* the grant, and the
|
|
23
|
+
* the grant, and the three options a name-only compare would hide. `admin` true is `WITH ADMIN`;
|
|
24
|
+
* `inherit` true inherits the parent's privileges even when the member is NOINHERIT;
|
|
24
25
|
* `set` true (the `GRANT` default) lets the member `SET ROLE` to the parent. `grantor` is null
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* only in an inconsistent catalog or a synthetic test row, not after an ordinary DROP ROLE:
|
|
27
|
+
* PostgreSQL tracks grantor dependencies. Keep the null guard as a defensive refusal. */
|
|
27
28
|
export interface MembershipRow {
|
|
28
29
|
readonly parent: string;
|
|
29
30
|
readonly grantor: string | null;
|
|
30
31
|
readonly admin: boolean;
|
|
31
32
|
readonly set: boolean;
|
|
33
|
+
readonly inherit: boolean;
|
|
32
34
|
}
|
|
33
35
|
export declare const selectRoleMemberships: (pg: PgExecutor, name: string) => Effect.Effect<readonly MembershipRow[], SqlError>;
|
|
34
36
|
/** Seat grants say `SET FALSE`. Upstream `GRANT` defaults `SET` to TRUE, and `SET ROLE` to the
|
|
35
37
|
* parent is how `SUPERUSER` / `CREATEROLE` / `CREATEDB` are exercised (`user-manag.sgml`). */
|
|
36
38
|
export declare const buildGrantMembershipSql: (member: string, parent: string) => string;
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
39
|
+
/** ⛔ Do not repair with GRANT … ADMIN FALSE: measured in PR 336, that left grants made
|
|
40
|
+
* by the member standing after stripping its ADMIN. PG 16+ tracks dependent role grants:
|
|
41
|
+
* https://www.postgresql.org/docs/16/sql-grant.html#SQL-GRANT-DESCRIPTION-ROLES
|
|
42
|
+
* https://www.postgresql.org/docs/16/sql-revoke.html
|
|
43
|
+
* Measured 2026-10-02 on PG 17.11: REVOKE ADMIN OPTION … RESTRICT refuses dependents;
|
|
44
|
+
* without dependents, ADMIN/SET revocation keeps membership and its INHERIT bit intact.
|
|
45
|
+
* INHERIT revocation explicitly clears that bit for a declared NOINHERIT role.
|
|
46
|
+
* GRANTED BY targets the observed row, including a different grantor's row. */
|
|
47
|
+
export declare const buildRepairMembershipSql: (member: string, parent: string, grantor: string, option: 'ADMIN' | 'SET' | 'INHERIT') => string;
|
|
41
48
|
/** Remove one grantor's row for one parent. Naming the grantor is what makes the revoke
|
|
42
49
|
* bind: a plain `REVOKE` skips rows other grantors made (`revoke.sgml`). */
|
|
43
50
|
export declare const buildRevokeGrantorMembershipSql: (member: string, parent: string, grantor: string) => string;
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
export declare const unsafeMemberships: (live: Omit<PostgresRoleAttributes, 'passwordSeal'
|
|
51
|
+
/** ADMIN/SET are always unsafe; per-grant INHERIT is unsafe for declared NOINHERIT.
|
|
52
|
+
* PG 16–18 GRANT docs: the role attribute only defaults NEW membership grants. */
|
|
53
|
+
export declare const unsafeMemberships: (live: Omit<PostgresRoleAttributes, 'passwordSeal'>,
|
|
54
|
+
/** The DECLARED inherit flag (never the live one): a declared NOINHERIT role must see live
|
|
55
|
+
* per-grant INHERIT rows as unsafe even when the live role flag says INHERIT. */
|
|
56
|
+
inherit: boolean) => ReadonlySet<string>;
|
|
47
57
|
/** Membership drift: sorted set difference between declared and live. Duplicates in the
|
|
48
58
|
* declaration collapse to one; `undefined` means "not asserted" — nothing to grant or revoke.
|
|
49
59
|
* A parent already granted `WITH ADMIN`, or with `SET` still at the upstream default TRUE, is
|
|
@@ -56,20 +66,13 @@ export declare const membershipDrift: (declared: readonly string[] | undefined,
|
|
|
56
66
|
* missing role is `42704` (`user.c@REL_18_6`), and on a create that error arrives only after
|
|
57
67
|
* `CREATE ROLE` has committed. */
|
|
58
68
|
export declare const assertParentsExist: (pg: PgExecutor, role: string, parents: readonly string[] | undefined) => Effect.Effect<void, PostgresRoleParentMissing | SqlError>;
|
|
59
|
-
/** Make live memberships equal the declaration,
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* 2. Unsafe-but-wanted rows get one full-options repair `GRANT` per parent — atomic, in place,
|
|
68
|
-
* so the membership never drops even when another grantor's row also exists.
|
|
69
|
-
* 3. A repair leaves other grantors' unsafe rows beside the safe one, so a mid re-read
|
|
70
|
-
* revokes every remaining unsafe row with a live grantor.
|
|
71
|
-
* 4. Absent-but-wanted parents get a fresh seat grant.
|
|
72
|
-
* 5. Backstop re-read: any row that is not wanted, or still unsafe, fails typed — including a
|
|
73
|
-
* dead-grantor row, which no statement this family may issue can repair. */
|
|
69
|
+
/** Make live memberships equal the declaration, preserving wanted memberships in place.
|
|
70
|
+
* The previous full-options GRANT updated only its own grantor's row, then revoked other
|
|
71
|
+
* unsafe rows; it could silently strip ADMIN despite dependent grants (PR 336 round 2).
|
|
72
|
+
* Now each unsafe option is revoked by its observed grantor with RESTRICT. Unwanted rows
|
|
73
|
+
* commit first, so a blocked ADMIN repair cannot restore unwanted access (PR 349 review).
|
|
74
|
+
* Each retained row's options share a transaction, attributing a dependency/permission
|
|
75
|
+
* refusal to that parent and grantor. No CASCADE and no replacement grantor row.
|
|
76
|
+
* Unnameable grantors are defensive catalog-corruption cases, left to the typed backstop. */
|
|
74
77
|
export declare const syncMemberships: (pg: PgExecutor, props: PostgresRoleProps, rows: readonly MembershipRow[]) => Effect.Effect<readonly string[], SqlError | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing>;
|
|
75
78
|
//# sourceMappingURL=role-membership-sql.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-membership-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-membership-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,gCAAgC,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEjF
|
|
1
|
+
{"version":3,"file":"role-membership-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-membership-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EAAE,gCAAgC,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEjF;;;;;yFAKyF;AACzF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAcD,eAAO,MAAM,qBAAqB,OAC5B,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,SAAS,aAAa,EAAE,EAAE,QAAQ,CAC2B,CAAC;AAE/E;8FAC8F;AAC9F,eAAO,MAAM,uBAAuB,WAAY,MAAM,UAAU,MAAM,KAAG,MACF,CAAC;AAExE;;;;;;;+EAO+E;AAC/E,eAAO,MAAM,wBAAwB,WAC3B,MAAM,UACN,MAAM,WACL,MAAM,UACP,OAAO,GAAG,KAAK,GAAG,SAAS,KAClC,MACwH,CAAC;AAE5H;4EAC4E;AAC5E,eAAO,MAAM,+BAA+B,WAClC,MAAM,UACN,MAAM,WACL,MAAM,KACd,MAC0F,CAAC;AAE9F;kFACkF;AAClF,eAAO,MAAM,iBAAiB,SACtB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC;AAClD;kFACkF;SACzE,OAAO,KACf,WAAW,CAAC,MAAM,CAKlB,CAAC;AAEJ;;;6FAG6F;AAC7F,eAAO,MAAM,eAAe,aAChB,SAAS,MAAM,EAAE,GAAG,SAAS,QACjC,SAAS,MAAM,EAAE,WACf,WAAW,CAAC,MAAM,CAAC,KAC1B;IAAE,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAW3E,CAAC;AAEF;;kCAEkC;AAClC,eAAO,MAAM,kBAAkB,OACzB,UAAU,QACR,MAAM,WACH,SAAS,MAAM,EAAE,GAAG,SAAS,KACrC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,QAAQ,CAQvD,CAAC;AAEL;;;;;;;6FAO6F;AAC7F,eAAO,MAAM,eAAe,OACtB,UAAU,SACP,iBAAiB,QAClB,SAAS,aAAa,EAAE,KAC7B,MAAM,CAAC,MAAM,CACd,SAAS,MAAM,EAAE,EACjB,QAAQ,GAAG,gCAAgC,GAAG,yBAAyB,CAuErE,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-provider.d.ts","sourceRoot":"","sources":["../../src/postgres/role-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAM7C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAOzC,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"role-provider.d.ts","sourceRoot":"","sources":["../../src/postgres/role-provider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAM7C,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAOzC,eAAO,MAAM,oBAAoB,4PAmF/B,CAAC;AAEH,eAAO,MAAM,oBAAoB,0HAA6D,CAAC"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `SCRAM-SHA-256$4096:<salt>$<StoredKey>:<ServerKey>` for one password. The salt is fresh per
|
|
3
3
|
* call (same as `psql \password`); tests pass one to make the output deterministic.
|
|
4
|
+
* Callers first run `assertPasswordAscii`: raw non-ASCII may differ from PostgreSQL's
|
|
5
|
+
* SASLprep bytes (https://www.postgresql.org/docs/16/sasl-authentication.html).
|
|
4
6
|
*/
|
|
5
7
|
export declare const scramSha256Verifier: (password: string, salt?: Buffer) => string;
|
|
6
8
|
//# sourceMappingURL=role-scram.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-scram.d.ts","sourceRoot":"","sources":["../../src/postgres/role-scram.ts"],"names":[],"mappings":"AA2BA
|
|
1
|
+
{"version":3,"file":"role-scram.d.ts","sourceRoot":"","sources":["../../src/postgres/role-scram.ts"],"names":[],"mappings":"AA2BA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,aAAc,MAAM,SAAS,MAAM,KAAG,MAMrE,CAAC"}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* written — a plan never re-sends a secret that already matches, and never stores one.
|
|
12
12
|
*/
|
|
13
13
|
import * as Redacted from 'effect/Redacted';
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import { PostgresRolePasswordNonAsciiRefused } from './role-errors.ts';
|
|
14
16
|
import { type Environment } from '../secrets/write-only.ts';
|
|
15
17
|
import type { PostgresRoleProps } from './role-attrs.ts';
|
|
16
18
|
/** What the environment holds for the declared password. `value` is `undefined` when no
|
|
@@ -20,6 +22,12 @@ export interface ResolvedPassword {
|
|
|
20
22
|
readonly variable: string | undefined;
|
|
21
23
|
readonly value: Redacted.Redacted<string> | undefined;
|
|
22
24
|
}
|
|
25
|
+
/** ⛔ PostgreSQL applies SASLprep before SCRAM, with a raw-byte fallback for prohibited input:
|
|
26
|
+
* https://www.postgresql.org/docs/16/sasl-authentication.html#SASL-SCRAM-SHA-256
|
|
27
|
+
* Non-ASCII can normalize to different bytes (e.g. soft hyphen disappears). Refuse before
|
|
28
|
+
* any write, even with a matching old seal, until we have a vendor-compatible normalizer.
|
|
29
|
+
* ASCII is unchanged, including prohibited ASCII controls via PostgreSQL's fallback. */
|
|
30
|
+
export declare const assertPasswordAscii: (role: string, resolved: ResolvedPassword) => Effect.Effect<void, PostgresRolePasswordNonAsciiRefused>;
|
|
23
31
|
export declare const resolvePassword: (props: PostgresRoleProps, env?: Environment) => ResolvedPassword;
|
|
24
32
|
/** `seal({ password })` of the resolved value — the one thing state remembers about it. */
|
|
25
33
|
export declare const sealPassword: (value: Redacted.Redacted<string>) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-secrets.d.ts","sourceRoot":"","sources":["../../src/postgres/role-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,KAAK,WAAW,EAAiC,MAAM,0BAA0B,CAAC;AAC3F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;2FAE2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACvD;AAED,eAAO,MAAM,eAAe,UACnB,iBAAiB,QACnB,WAAW,KACf,gBAQF,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,YAAY,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAG,MACrB,CAAC;AAE5C;;iFAEiF;AACjF,eAAO,MAAM,mBAAmB,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,MAAM,KAAG,OAC7B,CAAC"}
|
|
1
|
+
{"version":3,"file":"role-secrets.d.ts","sourceRoot":"","sources":["../../src/postgres/role-secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,mCAAmC,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,KAAK,WAAW,EAAiC,MAAM,0BAA0B,CAAC;AAC3F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD;;2FAE2F;AAC3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACvD;AAED;;;;wFAIwF;AACxF,eAAO,MAAM,mBAAmB,SACxB,MAAM,YACF,gBAAgB,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,mCAAmC,CAGzC,CAAC;AAElB,eAAO,MAAM,eAAe,UACnB,iBAAiB,QACnB,WAAW,KACf,gBAQF,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,YAAY,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAG,MACrB,CAAC;AAE5C;;iFAEiF;AACjF,eAAO,MAAM,mBAAmB,UAAW,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,MAAM,KAAG,OAC7B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEvB,MAAM,iBAAiB,CAAC;AAuBzB,eAAO,MAAM,UAAU,OACjB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,UAAU,GAAG,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAI7F,CAAC;AAEJ;oGACoG;AACpG,eAAO,MAAM,kBAAkB,OACzB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAW/E,CAAC;AAiBL,eAAO,MAAM,kBAAkB,UAAW,iBAAiB,KAAG,MACM,CAAC;AAErE,4FAA4F;AAC5F,eAAO,MAAM,iBAAiB,SACtB,MAAM,UAER;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KACxC,MAYF,CAAC;AAEF;;;;0BAI0B;AAC1B,eAAO,MAAM,mBAAmB,SAAU,MAAM,YAAY,MAAM,KAAG,MACW,CAAC;AAEjF,eAAO,MAAM,gBAAgB,SAAU,MAAM,KAAG,MAAmD,CAAC;AAEpG;sFACsF;AACtF,eAAO,MAAM,qBAAqB,UACzB,iBAAiB,YACd,MAAM,GAAG,SAAS,KAC3B,SAAS,MAAM,EAMjB,CAAC;AAEF;2FAC2F;AAC3F,eAAO,MAAM,WAAW,UACf,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,aAAa,CAAC,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAcvD,CAAC;AAEF;;wDAEwD;AACxD,eAAO,MAAM,kBAAkB,UACtB,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,
|
|
1
|
+
{"version":3,"file":"role-sql.d.ts","sourceRoot":"","sources":["../../src/postgres/role-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EAEvB,MAAM,iBAAiB,CAAC;AAuBzB,eAAO,MAAM,UAAU,OACjB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,UAAU,GAAG,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAI7F,CAAC;AAEJ;oGACoG;AACpG,eAAO,MAAM,kBAAkB,OACzB,UAAU,QACR,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,GAAG,SAAS,EAAE,QAAQ,CAW/E,CAAC;AAiBL,eAAO,MAAM,kBAAkB,UAAW,iBAAiB,KAAG,MACM,CAAC;AAErE,4FAA4F;AAC5F,eAAO,MAAM,iBAAiB,SACtB,MAAM,UAER;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GACnC;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;CACf,GACD;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KACxC,MAYF,CAAC;AAEF;;;;0BAI0B;AAC1B,eAAO,MAAM,mBAAmB,SAAU,MAAM,YAAY,MAAM,KAAG,MACW,CAAC;AAEjF,eAAO,MAAM,gBAAgB,SAAU,MAAM,KAAG,MAAmD,CAAC;AAEpG;sFACsF;AACtF,eAAO,MAAM,qBAAqB,UACzB,iBAAiB,YACd,MAAM,GAAG,SAAS,KAC3B,SAAS,MAAM,EAMjB,CAAC;AAEF;2FAC2F;AAC3F,eAAO,MAAM,WAAW,UACf,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,aAAa,CAAC,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAcvD,CAAC;AAEF;;wDAEwD;AACxD,eAAO,MAAM,kBAAkB,UACtB,iBAAiB,QAClB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,OAgBF,CAAC;AAEF;;;+CAG+C;AAC/C,eAAO,MAAM,gBAAgB,SACrB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAG7C,CAAC;AAEF;2DAC2D;AAC3D,eAAO,MAAM,eAAe,SACpB,IAAI,CAAC,sBAAsB,EAAE,cAAc,CAAC,KACjD,SAAS,MAAM,EAQjB,CAAC"}
|
package/dist/postgres/role.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ import * as Effect from 'effect/Effect';
|
|
|
31
31
|
import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
32
32
|
import { type Environment } from '../secrets/write-only.ts';
|
|
33
33
|
import type { PostgresRoleAttributes, PostgresRoleProps } from './role-attrs.ts';
|
|
34
|
-
import { PostgresRoleCreateVanished, PostgresRoleIdentityRefused, type PostgresRoleMembershipUnrepaired, PostgresRoleNameRefused, type PostgresRoleParentMissing, PostgresRolePasswordEnvUnsetError, PostgresRolePrivilegedRefused, PostgresRoleValidUntilRefused } from './role-errors.ts';
|
|
34
|
+
import { PostgresRoleCreateVanished, PostgresRoleIdentityRefused, type PostgresRoleMembershipUnrepaired, PostgresRoleNameRefused, type PostgresRoleParentMissing, PostgresRolePasswordEnvUnsetError, type PostgresRolePasswordNonAsciiRefused, PostgresRolePrivilegedRefused, PostgresRoleValidUntilRefused } from './role-errors.ts';
|
|
35
35
|
import { type PostgresConnection } from './connection.ts';
|
|
36
36
|
import type { PgExecutor } from './database-sql.ts';
|
|
37
37
|
export interface PostgresRole extends Resource<'Postgres.Role', PostgresRoleProps, PostgresRoleAttributes, never, PostgresConnection> {
|
|
@@ -47,7 +47,7 @@ export declare const isPostgresRole: (value: unknown) => value is PostgresRole;
|
|
|
47
47
|
* being written for the first time — because `pg_roles` answers no password (S25), so the live
|
|
48
48
|
* row cannot say whether the environment value still matches the one last written.
|
|
49
49
|
*/
|
|
50
|
-
export declare const reconcileWithClient: (pg: PgExecutor, props: PostgresRoleProps, env?: Environment, previousSeal?: string, storedOid?: number) => Effect.Effect<PostgresRoleAttributes, PostgresRolePasswordEnvUnsetError | PostgresRolePrivilegedRefused | PostgresRoleCreateVanished | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing | PostgresRoleIdentityRefused | SqlError>;
|
|
50
|
+
export declare const reconcileWithClient: (pg: PgExecutor, props: PostgresRoleProps, env?: Environment, previousSeal?: string, storedOid?: number) => Effect.Effect<PostgresRoleAttributes, PostgresRolePasswordEnvUnsetError | PostgresRolePasswordNonAsciiRefused | PostgresRolePrivilegedRefused | PostgresRoleCreateVanished | PostgresRoleMembershipUnrepaired | PostgresRoleParentMissing | PostgresRoleIdentityRefused | SqlError>;
|
|
51
51
|
/** The core of `read`. Answers `undefined` when absent; ownership branding and the state-only
|
|
52
52
|
* `passwordSeal` merge are the caller's. */
|
|
53
53
|
export declare const readRole: (pg: PgExecutor, name: string) => Effect.Effect<Omit<PostgresRoleAttributes, "passwordSeal"> | undefined, SqlError, never>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/postgres/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"role.d.ts","sourceRoot":"","sources":["../../src/postgres/role.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAmBjF,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,KAAK,gCAAgC,EACrC,uBAAuB,EACvB,KAAK,yBAAyB,EAC9B,iCAAiC,EACjC,KAAK,mCAAmC,EACxC,6BAA6B,EAC7B,6BAA6B,EAC9B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,YAAa,SAAQ,QAAQ,CAC5C,eAAe,EACf,iBAAiB,EACjB,sBAAsB,EACtB,KAAK,EACL,kBAAkB,CACnB;CAAG;AAEJ,eAAO,MAAM,YAAY,+CAEvB,CAAC;AAEH;iEACiE;AACjE,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,KAAK,IAAI,YAGD,CAAC;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,OAC1B,UAAU,SACP,iBAAiB,QACnB,WAAW,qCAEJ,MAAM,KACjB,MAAM,CAAC,MAAM,CACd,sBAAsB,EACpB,iCAAiC,GACjC,mCAAmC,GACnC,6BAA6B,GAC7B,0BAA0B,GAC1B,gCAAgC,GAChC,yBAAyB,GACzB,2BAA2B,GAC3B,QAAQ,CA0FR,CAAC;AAEL;4CAC4C;AAC5C,eAAO,MAAM,QAAQ,OAAQ,UAAU,QAAQ,MAAM,6FAAiC,CAAC;AAEvF;gDACgD;AAChD,eAAO,MAAM,YAAY,UAChB,iBAAiB,KACvB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,uBAAuB,GAAG,6BAA6B,CAkB1E,CAAC"}
|
package/docs/postgres-role.md
CHANGED
|
@@ -26,16 +26,26 @@ run yourself. The `Postgres.Database` resource in this kit already binds `@effec
|
|
|
26
26
|
never re-sends a secret that already matches, and never stores one. A create that declares a
|
|
27
27
|
password but whose variable is unset refuses before any write
|
|
28
28
|
(`PostgresRolePasswordEnvUnsetError`), including the retry of a create whose password statement
|
|
29
|
-
never sealed.
|
|
29
|
+
never sealed. Non-ASCII passwords fail typed before writes (`PostgresRolePasswordNonAsciiRefused`):
|
|
30
|
+
PostgreSQL applies SASLprep, and this provider does not implement that normalization.
|
|
30
31
|
- **Membership is `pg_auth_members`, compared as a sorted set.** `memberOf` omitted leaves live
|
|
31
32
|
memberships alone; `[]` ensures none. Moves run through one-parent `GRANT`/`REVOKE`
|
|
32
33
|
(`alter_role.sgml`: "there are no options for adding or removing memberships; use GRANT and
|
|
33
34
|
REVOKE"). A plain `REVOKE` only removes the session grantor's row (`plan_single_revoke` matches
|
|
34
35
|
`grantor`); each unwanted row is `REVOKE … GRANTED BY` its own grantor. A wanted row that is
|
|
35
|
-
`WITH ADMIN
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
`WITH ADMIN`, still `SET TRUE`, or `INHERIT TRUE` when `inherit:false` is declared is repaired
|
|
37
|
+
with `REVOKE ADMIN/SET/INHERIT OPTION FOR … GRANTED BY … RESTRICT`. Unwanted-row revokes commit
|
|
38
|
+
first, then each retained row's options share a repair transaction. SQLSTATE 2BP01/42501 becomes
|
|
39
|
+
`PostgresRoleMembershipUnrepaired` naming that parent and grantor, preserving refused options
|
|
40
|
+
and dependent rows without rolling back unwanted-row revokes. No CASCADE is issued.
|
|
41
|
+
Option repair was measured once by hand on a throwaway PostgreSQL 17.11 (2026-10-02);
|
|
42
|
+
automated tests stub the 2BP01/42501 responses. Semantics: PostgreSQL
|
|
43
|
+
[16 GRANT](https://www.postgresql.org/docs/16/sql-grant.html) / [REVOKE](https://www.postgresql.org/docs/16/sql-revoke.html),
|
|
44
|
+
[17 GRANT](https://www.postgresql.org/docs/17/sql-grant.html) / [REVOKE](https://www.postgresql.org/docs/17/sql-revoke.html),
|
|
45
|
+
[18 GRANT](https://www.postgresql.org/docs/18/sql-grant.html) / [REVOKE](https://www.postgresql.org/docs/18/sql-revoke.html).
|
|
46
|
+
`buildRepairMembershipSql` now requires the grantor and option (`ADMIN`, `SET` or `INHERIT`).
|
|
47
|
+
A null grantor is a defensive inconsistent-catalog case, not an ordinary DROP ROLE outcome,
|
|
48
|
+
and fails `PostgresRoleMembershipUnrepaired`. A missing parent fails
|
|
39
49
|
`PostgresRoleParentMissing` before any statement.
|
|
40
50
|
- **`defaultRemovalPolicy: 'retain'`.** A seat group role may own objects and be granted across
|
|
41
51
|
databases. A destroy is still implemented in full (`DROP ROLE IF EXISTS`, idempotent), opted in
|
|
@@ -47,7 +57,9 @@ run yourself. The `Postgres.Database` resource in this kit already binds `@effec
|
|
|
47
57
|
says `WITH SET FALSE`. Create runs `CREATE`, the password `ALTER` and the seat `GRANT`s in one
|
|
48
58
|
transaction, so a later statement failure does not leave a LOGIN role behind. `diff` reads
|
|
49
59
|
`pg_auth_members` live — stored attributes omit the option rows, and the engine passes stored
|
|
50
|
-
attributes — so `ADMIN` or `
|
|
60
|
+
attributes — so unsafe `ADMIN`, `SET` or `INHERIT` plans as `update`. Privileged catalog flags also plan
|
|
61
|
+
`update`, reaching the typed reconcile refusal. `read` omits membership option rows just as
|
|
62
|
+
reconcile does, so an unchanged role does not report structural drift. A live oid that is not the stored oid
|
|
51
63
|
is `PostgresRoleIdentityRefused` on read, diff, reconcile and delete: the name was recreated
|
|
52
64
|
out of band and is not dropped or altered.
|
|
53
65
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -208,7 +208,7 @@ describe('K-A4 finding 4: configValueProblem never accepts a flag-shaped value',
|
|
|
208
208
|
['a short flag', '-n'],
|
|
209
209
|
['a file-overwrite flag glued to its argument', '-o/etc/ceph/ceph.conf'],
|
|
210
210
|
['a long flag with =', '--admin-daemon=/var/run/ceph/ceph-mon.a.asok'],
|
|
211
|
-
['another long flag', '--conf=/tmp/x'],
|
|
211
|
+
['another long flag', '--conf=/tmp/x'], // tmp-allow: refused argv, not a directory this test creates
|
|
212
212
|
['the empty string', ''],
|
|
213
213
|
['a bare dash', '-'],
|
|
214
214
|
])('refuses %s (%j)', (_name, value) => {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from './sudo-allowlist.ts';
|
|
16
16
|
|
|
17
17
|
const DAEMONS = '/Library/LaunchDaemons';
|
|
18
|
-
const context = { prefixes: [DAEMONS, '/opt/example'], staged: '/tmp/hf-sudo-x/staged' };
|
|
18
|
+
const context = { prefixes: [DAEMONS, '/opt/example'], staged: '/tmp/hf-sudo-x/staged' }; // tmp-allow: in-memory staging path, not a directory this test creates
|
|
19
19
|
const allowed = (argv: string[]) => privilegedProblem(argv, context);
|
|
20
20
|
const PLIST = `${DAEMONS}/com.example.a.plist`;
|
|
21
21
|
|
|
@@ -66,7 +66,7 @@ describe('launchctl shapes', () => {
|
|
|
66
66
|
['bootstrap of two plists', [LAUNCHCTL, 'bootstrap', 'system', PLIST, PLIST]],
|
|
67
67
|
[
|
|
68
68
|
'bootstrap outside every prefix',
|
|
69
|
-
[LAUNCHCTL, 'bootstrap', 'system', '/tmp/com.example.a.plist'],
|
|
69
|
+
[LAUNCHCTL, 'bootstrap', 'system', '/tmp/com.example.a.plist'], // tmp-allow: refused argv, not a directory this test creates
|
|
70
70
|
],
|
|
71
71
|
['bootstrap of a non-plist', [LAUNCHCTL, 'bootstrap', 'system', `${DAEMONS}/com.example.a`]],
|
|
72
72
|
// ★ Only where launchd loads daemons at boot: anywhere else is a job that vanishes on restart.
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from './sudo-allowlist.ts';
|
|
23
23
|
|
|
24
24
|
const PREFIXES = ['/etc/systemd/system', '/usr/local/bin'];
|
|
25
|
-
const STAGED = '/tmp/hf-sudo-1/staged';
|
|
25
|
+
const STAGED = '/tmp/hf-sudo-1/staged'; // tmp-allow: in-memory staging path, not a directory this test creates
|
|
26
26
|
const TEMP = '/etc/systemd/system/.thing.service.hf-abcdef012345.tmp';
|
|
27
27
|
const context: AllowContext = { prefixes: PREFIXES, staged: STAGED, temp: TEMP };
|
|
28
28
|
const allowed = (argv: string[]) => privilegedProblem(argv, context);
|
|
@@ -51,7 +51,7 @@ describe('install shapes', () => {
|
|
|
51
51
|
'a target that is not the derived temp',
|
|
52
52
|
[INSTALL, '-m', '0644', '-T', '--', STAGED, '/etc/systemd/system/x.service'],
|
|
53
53
|
],
|
|
54
|
-
['a temp outside every prefix', [INSTALL, '-m', '0644', '-T', '--', STAGED, '/tmp/x.tmp']],
|
|
54
|
+
['a temp outside every prefix', [INSTALL, '-m', '0644', '-T', '--', STAGED, '/tmp/x.tmp']], // tmp-allow: refused argv, not a directory this test creates
|
|
55
55
|
['no -T', [INSTALL, '-m', '0644', '--', STAGED, TEMP]],
|
|
56
56
|
['a three-digit mode', [INSTALL, '-m', '644', '-T', '--', STAGED, TEMP]],
|
|
57
57
|
['-S (the Mac flag)', [INSTALL, '-S', '-m', '0644', '-T', '--', STAGED, TEMP]],
|
|
@@ -103,7 +103,7 @@ describe('mv and rm shapes', () => {
|
|
|
103
103
|
['mv without -T', [MV, '-f', '--', TEMP, '/etc/systemd/system/thing.service']],
|
|
104
104
|
['rm -r', [RM, '-rf', '--', TEMP]],
|
|
105
105
|
['rm without --', [RM, '-f', TEMP]],
|
|
106
|
-
['rm outside every prefix', [RM, '-f', '--', '/tmp/x']],
|
|
106
|
+
['rm outside every prefix', [RM, '-f', '--', '/tmp/x']], // tmp-allow: refused argv, not a directory this test creates
|
|
107
107
|
['sudo in argv', ['/usr/bin/sudo', '-n', RM, '-f', '--', TEMP]],
|
|
108
108
|
])('refuses %s', (_name, argv) => {
|
|
109
109
|
expect(allowed(argv)).toBeString();
|
|
@@ -52,7 +52,7 @@ describe('writeFileAtomic under a prefix', () => {
|
|
|
52
52
|
for (const line of logs) expect(line.startsWith('homeflare/linux sudo -n ')).toBe(true);
|
|
53
53
|
// No temp file left behind, and the staging directory is gone too.
|
|
54
54
|
expect([...fake.files.keys()].some((path) => path.includes('.hf-'))).toBe(false);
|
|
55
|
-
expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false);
|
|
55
|
+
expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false); // tmp-allow: in-memory fake path from fake-sudo, not a directory this test creates
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
test('a failed mv removes the temp it just installed, then still throws', async () => {
|
|
@@ -68,7 +68,7 @@ describe('writeFileAtomic under a prefix', () => {
|
|
|
68
68
|
expect(calls[2]?.at(-1)).toBe(calls[0]?.at(-1));
|
|
69
69
|
expect(fake.files.has(DEST)).toBe(false);
|
|
70
70
|
expect([...fake.files.keys()].some((path) => path.includes('.hf-'))).toBe(false);
|
|
71
|
-
expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false);
|
|
71
|
+
expect([...fake.dirs.keys()].some((path) => path.startsWith('/tmp/hf-sudo-'))).toBe(false); // tmp-allow: in-memory fake path from fake-sudo, not a directory this test creates
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
// 🔴 Adversarial review, round 2: a failed CLEANUP rm after a failed mv used to be swallowed
|
|
@@ -22,11 +22,14 @@ export interface FakeRoleState {
|
|
|
22
22
|
/** Bare role names, backing the `roleExists` check the database family's owner probe uses. */
|
|
23
23
|
readonly roleNames: Set<string>;
|
|
24
24
|
readonly roleRows: Map<string, PostgresRoleAttributes>;
|
|
25
|
-
/** `member\0parent\0grantor` triples backing `pg_auth_members` (`''` grantor =
|
|
25
|
+
/** `member\0parent\0grantor` triples backing `pg_auth_members` (`''` grantor = unresolvable
|
|
26
26
|
* grantor); `GRANT`/`REVOKE` mutate it. */
|
|
27
27
|
readonly memberships: Set<string>;
|
|
28
28
|
/** Options a name-only compare would hide, keyed `member\0parent\0grantor`. */
|
|
29
|
-
readonly membershipOptions: Map<
|
|
29
|
+
readonly membershipOptions: Map<
|
|
30
|
+
string,
|
|
31
|
+
{ readonly admin: boolean; readonly set: boolean; readonly inherit?: boolean }
|
|
32
|
+
>;
|
|
30
33
|
/** Hands out the next `oid` a real cluster would assign. */
|
|
31
34
|
nextOid(): number;
|
|
32
35
|
}
|
|
@@ -106,6 +109,7 @@ export const applyRoleStatement = <A extends object>(
|
|
|
106
109
|
grantor: parts[2] === undefined || parts[2] === '' ? null : (parts[2] as string),
|
|
107
110
|
admin: options?.admin === true,
|
|
108
111
|
set: options?.set === true,
|
|
112
|
+
inherit: options?.inherit === true,
|
|
109
113
|
};
|
|
110
114
|
})
|
|
111
115
|
.sort(
|
|
@@ -165,17 +169,33 @@ export const applyRoleStatement = <A extends object>(
|
|
|
165
169
|
// `grant.sgml` (REL_18_6): altering an existing membership retains every option the new
|
|
166
170
|
// GRANT omits, so only the full-options form clears ADMIN; `WITH SET FALSE` alone keeps a
|
|
167
171
|
// current ADMIN. A bare `GRANT` (never issued by this family) leaves options untouched.
|
|
168
|
-
const options = state.membershipOptions.get(key) ?? {
|
|
172
|
+
const options = state.membershipOptions.get(key) ?? {
|
|
173
|
+
admin: false,
|
|
174
|
+
set: false,
|
|
175
|
+
inherit: state.roleRows.get(member)?.inherit ?? true,
|
|
176
|
+
};
|
|
169
177
|
if (text.endsWith('WITH ADMIN FALSE, SET FALSE')) {
|
|
170
|
-
state.membershipOptions.
|
|
178
|
+
state.membershipOptions.set(key, { ...options, admin: false, set: false });
|
|
171
179
|
} else if (text.endsWith('WITH SET FALSE')) {
|
|
172
|
-
|
|
173
|
-
else state.membershipOptions.delete(key);
|
|
180
|
+
state.membershipOptions.set(key, { ...options, set: false });
|
|
174
181
|
}
|
|
175
182
|
return Effect.succeed([] as unknown as ReadonlyArray<A>);
|
|
176
183
|
}
|
|
177
184
|
|
|
178
185
|
if (text.startsWith('REVOKE')) {
|
|
186
|
+
const option =
|
|
187
|
+
/^REVOKE (ADMIN|SET|INHERIT) OPTION FOR "((?:[^"]|"")*)" FROM "((?:[^"]|"")*)" GRANTED BY "((?:[^"]|"")*)" RESTRICT$/.exec(
|
|
188
|
+
text,
|
|
189
|
+
);
|
|
190
|
+
if (option !== null) {
|
|
191
|
+
const key = `${unquoteIdent(option[3] as string)}\0${unquoteIdent(option[2] as string)}\0${unquoteIdent(option[4] as string)}`;
|
|
192
|
+
const options = state.membershipOptions.get(key) ?? { admin: false, set: false };
|
|
193
|
+
const next = { ...options, [(option[1] as string).toLowerCase()]: false };
|
|
194
|
+
if (next.admin || next.set || next.inherit !== undefined)
|
|
195
|
+
state.membershipOptions.set(key, next);
|
|
196
|
+
else state.membershipOptions.delete(key);
|
|
197
|
+
return Effect.succeed([] as unknown as ReadonlyArray<A>);
|
|
198
|
+
}
|
|
179
199
|
const granted =
|
|
180
200
|
/^REVOKE "((?:[^"]|"")*)" FROM "((?:[^"]|"")*)" GRANTED BY "((?:[^"]|"")*)"$/.exec(text);
|
|
181
201
|
if (granted !== null) {
|
package/src/postgres/fake-sql.ts
CHANGED
|
@@ -35,12 +35,14 @@ export interface FakeSql extends PgExecutor {
|
|
|
35
35
|
readonly roleRows: Map<string, PostgresRoleAttributes>;
|
|
36
36
|
/** `member\0parent\0grantor` triples backing `pg_auth_members` (a plain `pg_auth_members`
|
|
37
37
|
* read, a grant, a revoke and a DROP ROLE cascade all go through `fake-role-sql.ts`);
|
|
38
|
-
* `GRANT`/`REVOKE` mutate it. An empty grantor segment is
|
|
39
|
-
* exists. */
|
|
38
|
+
* `GRANT`/`REVOKE` mutate it. An empty grantor segment is an inconsistent catalog/test row. */
|
|
40
39
|
readonly memberships: Set<string>;
|
|
41
40
|
/** Options on a membership the name alone hides, keyed `member\0parent\0grantor`. Absent
|
|
42
|
-
* means neither ADMIN nor
|
|
43
|
-
readonly membershipOptions: Map<
|
|
41
|
+
* means neither ADMIN, SET nor an explicit per-grant INHERIT. */
|
|
42
|
+
readonly membershipOptions: Map<
|
|
43
|
+
string,
|
|
44
|
+
{ readonly admin: boolean; readonly set: boolean; readonly inherit?: boolean }
|
|
45
|
+
>;
|
|
44
46
|
/** Mutable on purpose: a test seeds a live schema (adoption, drift) or clears one (drop). */
|
|
45
47
|
readonly schemas: Map<string, PostgresSchemaAttributes>;
|
|
46
48
|
/** Mutable on purpose: names of schemas this fake pretends hold at least one relation, so a
|
|
@@ -85,7 +87,10 @@ export const makeFakeSql = (options: FakeSqlOptions = {}): FakeSql => {
|
|
|
85
87
|
const roleRows = new Map(options.roleRows?.map((r) => [r.name, r] as const) ?? []);
|
|
86
88
|
const databases = new Map(options.databases?.map((d) => [d.name, d] as const) ?? []);
|
|
87
89
|
const memberships = new Set<string>();
|
|
88
|
-
const membershipOptions = new Map<
|
|
90
|
+
const membershipOptions = new Map<
|
|
91
|
+
string,
|
|
92
|
+
{ readonly admin: boolean; readonly set: boolean; readonly inherit?: boolean }
|
|
93
|
+
>();
|
|
89
94
|
const schemas = seedSchemas(options.schemas, options.database ?? 'postgres');
|
|
90
95
|
const relationsIn = new Set(options.schemasWithRelations ?? []);
|
|
91
96
|
let raceRemaining = options.raceNextCreate === true ? 1 : 0;
|
package/src/postgres/index.ts
CHANGED
|
@@ -62,7 +62,8 @@ export interface PostgresRoleProps {
|
|
|
62
62
|
readonly login: boolean;
|
|
63
63
|
/** `CONNECTION LIMIT n`, `-1` for unlimited — maps to `pg_roles.rolconnlimit`. */
|
|
64
64
|
readonly connectionLimit: number;
|
|
65
|
-
/** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit
|
|
65
|
+
/** `INHERIT` or `NOINHERIT` — maps to `pg_roles.rolinherit`; false also clears per-grant
|
|
66
|
+
* INHERIT on declared memberships (PG 16+ uses the role flag only to default new grants). */
|
|
66
67
|
readonly inherit: boolean;
|
|
67
68
|
/**
|
|
68
69
|
* `VALID UNTIL 'timestamp'` — maps to `pg_roles.rolvaliduntil`. ISO 8601 without a timezone is
|
|
@@ -112,18 +113,20 @@ export interface PostgresRoleAttributes {
|
|
|
112
113
|
*/
|
|
113
114
|
readonly memberOf: readonly string[] | null;
|
|
114
115
|
/**
|
|
115
|
-
* The membership rows behind `memberOf`, including `admin_option
|
|
116
|
+
* The membership rows behind `memberOf`, including `admin_option`, `set_option` and
|
|
117
|
+
* `inherit_option`. Present
|
|
116
118
|
* on a catalog read; stripped before attributes are stored (`storedAttributes`), because the
|
|
117
119
|
* engine's diff receives stored attributes and would never see them there. The provider's
|
|
118
120
|
* diff reads these rows live. `grantor` is the role that made the grant
|
|
119
|
-
* (`pg_auth_members.grantor`)
|
|
120
|
-
* be named in `REVOKE … GRANTED BY
|
|
121
|
+
* (`pg_auth_members.grantor`). Null is a defensive inconsistent-catalog case, not an ordinary
|
|
122
|
+
* DROP ROLE outcome; such a row cannot be named in `REVOKE … GRANTED BY`.
|
|
121
123
|
*/
|
|
122
124
|
readonly memberships?: readonly {
|
|
123
125
|
readonly parent: string;
|
|
124
126
|
readonly grantor: string | null;
|
|
125
127
|
readonly admin: boolean;
|
|
126
128
|
readonly set: boolean;
|
|
129
|
+
readonly inherit: boolean;
|
|
127
130
|
}[];
|
|
128
131
|
/**
|
|
129
132
|
* A seal of the last password value written, or `''` if none was ever written (`seal()` from
|