@jantstack/adonis-authz 2.4.0-alpha.1 → 2.4.0-alpha.2
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/README.md +248 -18
- package/build/commands/authz_relations_reconcile.d.ts +25 -1
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
- package/build/commands/authz_relations_reconcile.js +94 -6
- package/build/commands/authz_relations_reconcile.js.map +1 -1
- package/build/index.d.ts +12 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +10 -0
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +7 -0
- package/build/providers/authz_provider.js.map +1 -1
- package/build/src/define_config.d.ts +36 -0
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts +53 -14
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +104 -32
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +47 -9
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/database_relations_driver.js +318 -89
- package/build/src/drivers/database_relations_driver.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts +17 -4
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +38 -3
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +15 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -1
- package/build/src/drivers/openfga_facts.js +25 -2
- package/build/src/drivers/openfga_facts.js.map +1 -1
- package/build/src/drivers/openfga_relations_driver.d.ts +27 -6
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_relations_driver.js +140 -16
- package/build/src/drivers/openfga_relations_driver.js.map +1 -1
- package/build/src/errors.d.ts +35 -3
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +61 -3
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +37 -16
- package/build/src/freeze.d.ts.map +1 -1
- package/build/src/freeze.js +53 -8
- package/build/src/freeze.js.map +1 -1
- package/build/src/manager.d.ts +16 -1
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +116 -25
- package/build/src/manager.js.map +1 -1
- package/build/src/models/authz_assignment.d.ts +2 -2
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +2 -2
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +2 -2
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_role.d.ts +2 -2
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +2 -2
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/relation_partition_trigger.d.ts.map +1 -1
- package/build/src/relation_partition_trigger.js +11 -3
- package/build/src/relation_partition_trigger.js.map +1 -1
- package/build/src/relations/define_relations_config.d.ts +17 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -1
- package/build/src/relations/define_relations_config.js +30 -1
- package/build/src/relations/define_relations_config.js.map +1 -1
- package/build/src/relations/manager.d.ts +25 -3
- package/build/src/relations/manager.d.ts.map +1 -1
- package/build/src/relations/manager.js +76 -19
- package/build/src/relations/manager.js.map +1 -1
- package/build/src/relations/reconcile.d.ts +53 -4
- package/build/src/relations/reconcile.d.ts.map +1 -1
- package/build/src/relations/reconcile.js +92 -30
- package/build/src/relations/reconcile.js.map +1 -1
- package/build/src/scope_outbox.d.ts.map +1 -1
- package/build/src/scope_outbox.js +17 -10
- package/build/src/scope_outbox.js.map +1 -1
- package/build/src/shared/backend_guard.d.ts +28 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -1
- package/build/src/shared/backend_guard.js +52 -0
- package/build/src/shared/backend_guard.js.map +1 -1
- package/build/src/shared/transaction_guard.d.ts +50 -0
- package/build/src/shared/transaction_guard.d.ts.map +1 -0
- package/build/src/shared/transaction_guard.js +60 -0
- package/build/src/shared/transaction_guard.js.map +1 -0
- package/build/src/testing/contract.d.ts +55 -4
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +240 -8
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +4 -4
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +2 -2
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/relations_contract.d.ts +23 -1
- package/build/src/testing/relations_contract.d.ts.map +1 -1
- package/build/src/testing/relations_contract.js +432 -17
- package/build/src/testing/relations_contract.js.map +1 -1
- package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -1
- package/build/src/testing/relations_reconcile_contract.js +49 -1
- package/build/src/testing/relations_reconcile_contract.js.map +1 -1
- package/build/src/traits/has_uuid.d.ts +2 -2
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +197 -10
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js.map +1 -1
- package/build/stubs/migration.stub +20 -10
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import '@japa/assert';
|
|
2
2
|
import type { Assert } from '@japa/assert';
|
|
3
|
-
import type { AuthorizationDriver, CatalogSpec } from '../types.js';
|
|
3
|
+
import type { AuthorizationDriver, CatalogSpec, SubjectRef } from '../types.js';
|
|
4
4
|
import type { ContractScopeTree } from './scope_tree.js';
|
|
5
5
|
/**
|
|
6
6
|
* Suite de contrato del sistema de autorización — EL JUEZ.
|
|
@@ -12,7 +12,7 @@ import type { ContractScopeTree } from './scope_tree.js';
|
|
|
12
12
|
* runAuthorizationDriverContract({
|
|
13
13
|
* name: 'mi-driver',
|
|
14
14
|
* level: '2.0', // omitido = 'core': solo los casos de 1.x
|
|
15
|
-
* capabilities: { hierarchyFacts: false,
|
|
15
|
+
* capabilities: { hierarchyFacts: false, transactionalWrites: false, truncationSignal: false,
|
|
16
16
|
* singleCheckAuthorize: false, injectableClock: false },
|
|
17
17
|
* seedCatalog: (catalog) => ..., // materializa roles/permisos
|
|
18
18
|
* makeDriver: (tree) => new MiDriver({ resolveChain: ... }), // recibe el árbol
|
|
@@ -41,8 +41,29 @@ import type { ContractScopeTree } from './scope_tree.js';
|
|
|
41
41
|
export interface DriverCapabilities {
|
|
42
42
|
/** El driver materializa el árbol como hechos propios (Fase 3b). */
|
|
43
43
|
hierarchyFacts: boolean;
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* **`{ transaction }` en `grant`/`revoke`/`deny`/`removeDeny` ESCRIBE en
|
|
46
|
+
* la transacción del consumidor** — «los dos o ninguno con TU transacción»,
|
|
47
|
+
* nunca «no se pierde» (L-2, panel `{trx}` (C); mismo nombre que
|
|
48
|
+
* `AuthorizationDriverCapabilities.transactionalWrites` y que su par de
|
|
49
|
+
* relaciones). Es el ÚNICO par del juez con las DOS caras obligatorias:
|
|
50
|
+
* `true` sin caso `whenTrue` Y `false` sin caso `whenFalse` hacen que la
|
|
51
|
+
* suite lance al registrarse (los demás pares solo exigen la cara `true`;
|
|
52
|
+
* la lección de 4-2 es que una `whenFalse` ausente se salta en silencio).
|
|
53
|
+
* Con `false` el juez fija que `{ transaction }` es 500 `E_AUTHZ_UNSUPPORTED`
|
|
54
|
+
* nombrando driver y operación con CERO llamadas al driver (espía), y que
|
|
55
|
+
* `requireTransactionalWrites: true` lo convierte en 500 `E_AUTHZ_CONFIG`
|
|
56
|
+
* al RESOLVER. Con `true` (L-3) el juez abre una transacción de la conexión
|
|
57
|
+
* del driver (`DriverContractHarness.transactions`, default Lucid +
|
|
58
|
+
* `authz_*`) y fija, para las cuatro escrituras, que **rollback ⇒ CERO
|
|
59
|
+
* filas por CENSO** (no solo `authorize`), commit ⇒ aplicado, la escritura
|
|
60
|
+
* sin confirmar invisible para la autoridad, y que una transacción ajena es
|
|
61
|
+
* 500 `E_AUTHZ_CONFIG` sin una sentencia. `database` declara `true` (con
|
|
62
|
+
* pool ≥ 2; en `:memory:` el harness del paquete declara `false` porque la
|
|
63
|
+
* autoridad no tendría por dónde leer); `openfga` no puede ser otra cosa que
|
|
64
|
+
* `false`: una tupla no entra en una transacción SQL.
|
|
65
|
+
*/
|
|
66
|
+
transactionalWrites: boolean;
|
|
46
67
|
/**
|
|
47
68
|
* Los `list*` lanzan si el backend trunca. Ningún driver del paquete trunca
|
|
48
69
|
* (L0.7 se cerró enumerando con `Read` paginado), así que no hay caso para
|
|
@@ -207,7 +228,31 @@ export interface DriverContractHarness {
|
|
|
207
228
|
*/
|
|
208
229
|
makeTwin?(driver: AuthorizationDriver, tree: ContractScopeTree): AuthorizationDriver | Promise<AuthorizationDriver>;
|
|
209
230
|
cleanup(): Promise<void>;
|
|
231
|
+
/**
|
|
232
|
+
* Solo con `transactionalWrites: true` (L-3): cómo abre el juez una
|
|
233
|
+
* transacción de LA CONEXIÓN DEL DRIVER y cómo cuenta los hechos de un
|
|
234
|
+
* holder en el backend — el CENSO: el rollback se juzga por filas, no solo
|
|
235
|
+
* por la respuesta de `authorize` (la lección del contrato de migración).
|
|
236
|
+
* Default: `db.transaction()` de Lucid (conexión primaria) y
|
|
237
|
+
* `authz_assignments`/`authz_denies`, lo que vale para el driver `database`.
|
|
238
|
+
* Un driver de terceros con otra conexión u otras tablas trae el suyo.
|
|
239
|
+
*/
|
|
240
|
+
transactions?: ContractTransactions;
|
|
210
241
|
}
|
|
242
|
+
/** Ver `DriverContractHarness.transactions`. */
|
|
243
|
+
export interface ContractTransactions {
|
|
244
|
+
begin(): Promise<{
|
|
245
|
+
client: unknown;
|
|
246
|
+
commit(): Promise<unknown>;
|
|
247
|
+
rollback(): Promise<unknown>;
|
|
248
|
+
}>;
|
|
249
|
+
census(subject: SubjectRef): Promise<{
|
|
250
|
+
assignments: number;
|
|
251
|
+
denies: number;
|
|
252
|
+
}>;
|
|
253
|
+
}
|
|
254
|
+
/** El default de `DriverContractHarness.transactions`: Lucid + `authz_*`. */
|
|
255
|
+
export declare function lucidContractTransactions(): ContractTransactions;
|
|
211
256
|
/**
|
|
212
257
|
* Lo mínimo que el juez necesita del runner. En producción es el `test` de
|
|
213
258
|
* Japa; en `tests/contract_harness.spec.ts` es una API falsa que solo anota
|
|
@@ -229,4 +274,10 @@ export interface ContractTestApi {
|
|
|
229
274
|
}
|
|
230
275
|
export declare function runAuthorizationDriverContract(harness: DriverContractHarness): void;
|
|
231
276
|
export declare function registerAuthorizationDriverContract(harness: DriverContractHarness, api: ContractTestApi): void;
|
|
277
|
+
/**
|
|
278
|
+
* La regla del guard, PURA (para que `contract_harness.spec.ts` la juzgue sin
|
|
279
|
+
* borrar caras): una capacidad `true` sin caso registrado queda sin cubrir;
|
|
280
|
+
* y una de `BOTH_FACES_REQUIRED` queda sin cubrir con CUALQUIER valor.
|
|
281
|
+
*/
|
|
282
|
+
export declare function uncoveredCapabilities(capabilities: DriverCapabilities, covered: ReadonlySet<keyof DriverCapabilities>): (keyof DriverCapabilities)[];
|
|
232
283
|
//# sourceMappingURL=contract.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/testing/contract.ts"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAG1C,OAAO,KAAK,EACV,mBAAmB,EAEnB,WAAW,
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../../../src/testing/contract.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,26 @@ import db from '@adonisjs/lucid/services/db';
|
|
|
7
7
|
import { CatalogCache, GLOBAL_OWNER_KEY, withAuthzCatalogWrite } from '../catalog/catalog_cache.js';
|
|
8
8
|
import { scopeKey } from '../identity.js';
|
|
9
9
|
import { descendantsFrom, memoryScopeTree, resolveChainFrom } from './scope_tree.js';
|
|
10
|
+
/** El default de `DriverContractHarness.transactions`: Lucid + `authz_*`. */
|
|
11
|
+
export function lucidContractTransactions() {
|
|
12
|
+
const count = async (table, subject) => {
|
|
13
|
+
const rows = await db
|
|
14
|
+
.from(table)
|
|
15
|
+
.where('holder_type', subject.type)
|
|
16
|
+
.where('holder_uuid', subject.uuid)
|
|
17
|
+
.count('* as n');
|
|
18
|
+
return Number(rows[0]?.n ?? 0);
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
async begin() {
|
|
22
|
+
const trx = await db.transaction();
|
|
23
|
+
return { client: trx, commit: () => trx.commit(), rollback: () => trx.rollback() };
|
|
24
|
+
},
|
|
25
|
+
async census(subject) {
|
|
26
|
+
return { assignments: await count('authz_assignments', subject), denies: await count('authz_denies', subject) };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
10
30
|
/**
|
|
11
31
|
* Catálogo de prueba: roles a nivel app y a nivel organization. Los dos
|
|
12
32
|
* últimos existen solo para el invariante 8: `rank` es metadata y el motor no
|
|
@@ -1689,8 +1709,9 @@ export function registerAuthorizationDriverContract(harness, api) {
|
|
|
1689
1709
|
}
|
|
1690
1710
|
});
|
|
1691
1711
|
// ── Concurrencia (2.5 · J4) ──────────────────────────────────────────
|
|
1692
|
-
// Sin `{trx}` en
|
|
1693
|
-
//
|
|
1712
|
+
// Sin `{trx}` en estos casos (la escritura en la transacción del
|
|
1713
|
+
// llamante tiene su par, `transactionalWrites`, abajo): lo que se fija
|
|
1714
|
+
// es que dos escrituras solapadas nunca dejan un
|
|
1694
1715
|
// estado que ninguna de las dos habría dejado sola. Con el harness en
|
|
1695
1716
|
// memoria (pool 1) se solapan a nivel de consulta; con `sqlite-file`, PG
|
|
1696
1717
|
// y MySQL (pool ≥ 2) a nivel de conexión.
|
|
@@ -3255,9 +3276,201 @@ export function registerAuthorizationDriverContract(harness, api) {
|
|
|
3255
3276
|
// `hierarchyFacts: false` → lo observan N1b/N2/N4/N5 (arriba): el driver
|
|
3256
3277
|
// responde según el árbol que le resuelve el consumidor, y `tree.move`
|
|
3257
3278
|
// cambia la respuesta sin escritura. `true` llega en Fase 3b.
|
|
3258
|
-
// `injectableClock` tiene su par arriba (2.5 · J1). `
|
|
3259
|
-
//
|
|
3260
|
-
//
|
|
3279
|
+
// `injectableClock` tiene su par arriba (2.5 · J1). `transactionalWrites`
|
|
3280
|
+
// (L-2) tiene su par al final: la cara `false` hoy, la `true` con L-3.
|
|
3281
|
+
// ── L-2 · `transactionalWrites` (panel `{trx}`, (C)) ──────────────────
|
|
3282
|
+
// Las DOS caras son obligatorias en este par (`BOTH_FACES_REQUIRED`): una
|
|
3283
|
+
// `whenFalse` ausente se saltaba en silencio y «rechazo activo» sería
|
|
3284
|
+
// una etiqueta. Lo que la cara `false` fija es la PUERTA 1 (por llamada,
|
|
3285
|
+
// siempre activa) y la PUERTA 2 (por config, opt-in), las dos con el
|
|
3286
|
+
// manager construido sobre el driver del harness, en TODOS los niveles:
|
|
3287
|
+
// son composición del manager sobre `driver.capabilities`, así que un
|
|
3288
|
+
// driver de terceros de nivel `core` también las observa.
|
|
3289
|
+
caseFor('transactionalWrites', {
|
|
3290
|
+
// L-3: la cara `true`. Lo que se juzga es «los dos o ninguno con TU
|
|
3291
|
+
// transacción» POR CENSO: el rollback deja CERO filas nuevas (no solo
|
|
3292
|
+
// `authorize` false — una respuesta puede salir bien con la fila viva),
|
|
3293
|
+
// el commit las deja escritas, y mientras la transacción está abierta
|
|
3294
|
+
// la autoridad (que lee por la conexión del motor) no ve nada. Y la
|
|
3295
|
+
// otra mitad de la regla: una transacción que NO es la del driver no
|
|
3296
|
+
// recibe ni una sentencia. Cuatro escrituras, las dos caras cada una.
|
|
3297
|
+
whenTrue: () => {
|
|
3298
|
+
test('transactionalWrites: true · grant/revoke/deny/removeDeny con { transaction } + rollback ⇒ authorize sin cambio Y CERO filas (censo); + commit ⇒ aplicado; una transacción ajena, un cliente sin transacción o el `db` entero ⇒ 500 E_AUTHZ_CONFIG sin UNA sentencia por ella', async ({ assert, }) => {
|
|
3299
|
+
assert.equal(driver.capabilities?.transactionalWrites, true, 'el harness declara transactionalWrites: true y el driver no: declara lo observable');
|
|
3300
|
+
const tx = harness.transactions ?? lucidContractTransactions();
|
|
3301
|
+
const alice = subject();
|
|
3302
|
+
const org = await orgUnder(tree, APP_SCOPE);
|
|
3303
|
+
const authz = managerOver({});
|
|
3304
|
+
const can = () => driver.authorize(alice, 'docs:read', org);
|
|
3305
|
+
const census = () => tx.census(alice);
|
|
3306
|
+
// Una transacción que se queda abierta tras una aserción fallida
|
|
3307
|
+
// retiene una conexión del pool y arrastra a los casos siguientes:
|
|
3308
|
+
// el rollback va en `finally`, y el commit solo si nada falló.
|
|
3309
|
+
const rolledBack = async (fn) => {
|
|
3310
|
+
const t = await tx.begin();
|
|
3311
|
+
try {
|
|
3312
|
+
await fn(t.client);
|
|
3313
|
+
}
|
|
3314
|
+
finally {
|
|
3315
|
+
await t.rollback();
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
const committed = async (fn) => {
|
|
3319
|
+
const t = await tx.begin();
|
|
3320
|
+
try {
|
|
3321
|
+
await fn(t.client);
|
|
3322
|
+
}
|
|
3323
|
+
catch (error) {
|
|
3324
|
+
await t.rollback();
|
|
3325
|
+
throw error;
|
|
3326
|
+
}
|
|
3327
|
+
await t.commit();
|
|
3328
|
+
};
|
|
3329
|
+
// grant: dentro no se ve desde fuera; rollback ⇒ nada; commit ⇒ concede.
|
|
3330
|
+
let seenInside;
|
|
3331
|
+
await rolledBack(async (transaction) => {
|
|
3332
|
+
await authz.grant(alice, 'org-editor', org, { transaction });
|
|
3333
|
+
seenInside = await can();
|
|
3334
|
+
});
|
|
3335
|
+
assert.deepEqual(await census(), { assignments: 0, denies: 0 }, 'ROJO: la asignación sobrevivió al rollback (la escritura NO fue por la transacción del llamante)');
|
|
3336
|
+
assert.isFalse(await can(), 'grant + rollback ⇒ no concede');
|
|
3337
|
+
assert.isFalse(seenInside, 'la autoridad lee por la conexión del motor: el grant sin confirmar no concede');
|
|
3338
|
+
await committed(async (transaction) => {
|
|
3339
|
+
await authz.grant(alice, 'org-editor', org, { transaction });
|
|
3340
|
+
});
|
|
3341
|
+
assert.isTrue(await can(), 'grant + commit ⇒ concede');
|
|
3342
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 });
|
|
3343
|
+
// revoke: rollback ⇒ la asignación sigue; commit ⇒ se va.
|
|
3344
|
+
await rolledBack(async (transaction) => {
|
|
3345
|
+
await authz.revoke(alice, 'org-editor', org, { transaction });
|
|
3346
|
+
seenInside = await can();
|
|
3347
|
+
});
|
|
3348
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 }, 'ROJO: el revoke sobrevivió al rollback');
|
|
3349
|
+
assert.isTrue(await can(), 'revoke + rollback ⇒ sigue concediendo');
|
|
3350
|
+
assert.isTrue(seenInside, 'el revoke sin confirmar no se ve desde la conexión del motor');
|
|
3351
|
+
await committed(async (transaction) => {
|
|
3352
|
+
await authz.revoke(alice, 'org-editor', org, { transaction });
|
|
3353
|
+
});
|
|
3354
|
+
assert.isFalse(await can(), 'revoke + commit ⇒ no concede');
|
|
3355
|
+
assert.deepEqual(await census(), { assignments: 0, denies: 0 });
|
|
3356
|
+
// deny (sobre un grant confirmado): rollback ⇒ sigue concediendo; commit ⇒ deniega.
|
|
3357
|
+
await authz.grant(alice, 'org-editor', org);
|
|
3358
|
+
await rolledBack(async (transaction) => {
|
|
3359
|
+
await authz.deny(alice, 'docs:read', org, { transaction });
|
|
3360
|
+
seenInside = await can();
|
|
3361
|
+
});
|
|
3362
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 }, 'ROJO: el deny sobrevivió al rollback');
|
|
3363
|
+
assert.isTrue(await can(), 'deny + rollback ⇒ sigue concediendo');
|
|
3364
|
+
assert.isTrue(seenInside, 'el deny sin confirmar no se ve desde la conexión del motor');
|
|
3365
|
+
await committed(async (transaction) => {
|
|
3366
|
+
await authz.deny(alice, 'docs:read', org, { transaction });
|
|
3367
|
+
});
|
|
3368
|
+
assert.isFalse(await can(), 'deny + commit ⇒ deniega');
|
|
3369
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 1 });
|
|
3370
|
+
// removeDeny: rollback ⇒ el deny sigue; commit ⇒ restaura.
|
|
3371
|
+
await rolledBack(async (transaction) => {
|
|
3372
|
+
await authz.removeDeny(alice, 'docs:read', org, { transaction });
|
|
3373
|
+
seenInside = await can();
|
|
3374
|
+
});
|
|
3375
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 1 }, 'ROJO: el removeDeny sobrevivió al rollback');
|
|
3376
|
+
assert.isFalse(await can(), 'removeDeny + rollback ⇒ sigue denegando');
|
|
3377
|
+
assert.isFalse(seenInside, 'el removeDeny sin confirmar no se ve desde la conexión del motor');
|
|
3378
|
+
await committed(async (transaction) => {
|
|
3379
|
+
await authz.removeDeny(alice, 'docs:read', org, { transaction });
|
|
3380
|
+
});
|
|
3381
|
+
assert.isTrue(await can(), 'removeDeny + commit ⇒ restaura');
|
|
3382
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 });
|
|
3383
|
+
// Una transacción que NO es la del driver (`assertCallerTransaction`):
|
|
3384
|
+
// 500 `E_AUTHZ_CONFIG` y ni una sentencia por ella (espía).
|
|
3385
|
+
const statements = [];
|
|
3386
|
+
const spy = (shape) => new Proxy(shape, {
|
|
3387
|
+
get(target, prop) {
|
|
3388
|
+
if (prop === 'from' || prop === 'table' || prop === 'raw' || prop === 'query' || prop === 'knexQuery') {
|
|
3389
|
+
return (...args) => {
|
|
3390
|
+
statements.push(`${String(prop)}(${String(args[0] ?? '')})`);
|
|
3391
|
+
return { where: () => ({}), select: async () => [], insert: async () => [], delete: async () => 0 };
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
return Reflect.get(target, prop);
|
|
3395
|
+
},
|
|
3396
|
+
});
|
|
3397
|
+
const foreign = spy({ isTransaction: true, connectionName: `not-${harness.name}-${uuidv7()}` });
|
|
3398
|
+
const notATransaction = spy({ isTransaction: false, connectionName: 'primary' });
|
|
3399
|
+
const wholeDb = spy({ connection() { }, primaryConnectionName: 'primary' });
|
|
3400
|
+
const bob = subject();
|
|
3401
|
+
for (const [label, transaction] of [
|
|
3402
|
+
['otra conexión', foreign],
|
|
3403
|
+
['un cliente sin transacción', notATransaction],
|
|
3404
|
+
['el db entero', wholeDb],
|
|
3405
|
+
]) {
|
|
3406
|
+
for (const [operation, run] of [
|
|
3407
|
+
['grant', () => authz.grant(bob, 'org-editor', org, { transaction })],
|
|
3408
|
+
['revoke', () => authz.revoke(bob, 'org-editor', org, { transaction })],
|
|
3409
|
+
['deny', () => authz.deny(bob, 'docs:read', org, { transaction })],
|
|
3410
|
+
['removeDeny', () => authz.removeDeny(bob, 'docs:read', org, { transaction })],
|
|
3411
|
+
]) {
|
|
3412
|
+
const error = await rejectsWith(assert, run, { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3413
|
+
assert.include(error.message, operation, `${label}/${operation}: nombra la operación`);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
assert.deepEqual(statements, [], 'ni una sentencia por una transacción que no es la del driver');
|
|
3417
|
+
assert.deepEqual(await tx.census(bob), { assignments: 0, denies: 0 });
|
|
3418
|
+
assert.isFalse(await driver.authorize(bob, 'docs:read', org));
|
|
3419
|
+
})?.timeout(30_000);
|
|
3420
|
+
},
|
|
3421
|
+
whenFalse: () => {
|
|
3422
|
+
test('transactionalWrites: false · { transaction } en grant/revoke/deny/removeDeny es 500 E_AUTHZ_UNSUPPORTED nombrando driver y operación, con CERO llamadas al driver (espía); y requireTransactionalWrites: true ⇒ 500 E_AUTHZ_CONFIG al RESOLVER el driver', async ({ assert, }) => {
|
|
3423
|
+
// El harness declara `false`: el driver tiene que decir lo mismo
|
|
3424
|
+
// (el manager lee `driver.capabilities`, no el harness).
|
|
3425
|
+
assert.notEqual(driver.capabilities?.transactionalWrites, true, 'el harness declara transactionalWrites: false y el driver declara true: declara lo observable');
|
|
3426
|
+
const touched = [];
|
|
3427
|
+
const spied = new Proxy(driver, {
|
|
3428
|
+
get(target, prop, receiver) {
|
|
3429
|
+
const value = Reflect.get(target, prop, receiver);
|
|
3430
|
+
if (typeof value === 'function' && typeof prop === 'string') {
|
|
3431
|
+
return (...args) => {
|
|
3432
|
+
touched.push(prop);
|
|
3433
|
+
return value.apply(receiver, args);
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
return value;
|
|
3437
|
+
},
|
|
3438
|
+
});
|
|
3439
|
+
// La forma de un `TransactionClientContract` de Lucid: la puerta
|
|
3440
|
+
// decide por la CAPACIDAD, antes de mirar qué transacción es.
|
|
3441
|
+
const trx = { from() { }, table() { }, isTransaction: true, connectionName: 'primary' };
|
|
3442
|
+
const alice = subject();
|
|
3443
|
+
const org = await orgUnder(tree, APP_SCOPE);
|
|
3444
|
+
const authz = managerOver({}, spied);
|
|
3445
|
+
const writes = [
|
|
3446
|
+
['grant', () => authz.grant(alice, 'org-editor', org, { transaction: trx })],
|
|
3447
|
+
['revoke', () => authz.revoke(alice, 'org-editor', org, { transaction: trx })],
|
|
3448
|
+
['deny', () => authz.deny(alice, 'docs:read', org, { transaction: trx })],
|
|
3449
|
+
['removeDeny', () => authz.removeDeny(alice, 'docs:read', org, { transaction: trx })],
|
|
3450
|
+
];
|
|
3451
|
+
for (const [operation, run] of writes) {
|
|
3452
|
+
const error = await rejectsWith(assert, run, { status: 500, code: 'E_AUTHZ_UNSUPPORTED' });
|
|
3453
|
+
assert.include(error.message, `'${harness.name}'`, `${operation}: nombra el driver`);
|
|
3454
|
+
assert.include(error.message, operation, `${operation}: nombra la operación`);
|
|
3455
|
+
assert.include(error.message, 'requireTransactionalWrites', `${operation}: la letra lleva la salida`);
|
|
3456
|
+
}
|
|
3457
|
+
assert.deepEqual(touched, [], 'cero llamadas al driver: el rechazo va ANTES del backend');
|
|
3458
|
+
// Sin `{ transaction }` la MISMA escritura entra: la puerta es del
|
|
3459
|
+
// parámetro, no del driver.
|
|
3460
|
+
await authz.grant(alice, 'org-editor', org);
|
|
3461
|
+
assert.isTrue(await driver.authorize(alice, 'docs:read', org));
|
|
3462
|
+
assert.include(touched, 'grant');
|
|
3463
|
+
// Puerta 2, opt-in: quien quiera fallar al ARRANCAR lo pide, y
|
|
3464
|
+
// entonces el driver ni se resuelve (tampoco para leer).
|
|
3465
|
+
const strict = managerOver({ requireTransactionalWrites: true }, spied);
|
|
3466
|
+
const atResolve = await rejectsWith(assert, () => strict.driver(), { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3467
|
+
assert.include(atResolve.message, `'${harness.name}'`);
|
|
3468
|
+
assert.include(atResolve.message, 'transactionalWrites');
|
|
3469
|
+
await rejectsWith(assert, () => strict.authorize(alice, 'docs:read', org), { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3470
|
+
await rejectsWith(assert, () => strict.grant(alice, 'org-editor', org), { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3471
|
+
});
|
|
3472
|
+
},
|
|
3473
|
+
});
|
|
3261
3474
|
// ── 3b-2e · E6 · `singleCheckAuthorize` ──────────────────────────────
|
|
3262
3475
|
caseFor('singleCheckAuthorize', {
|
|
3263
3476
|
whenTrue: () => {
|
|
@@ -3626,11 +3839,30 @@ export function registerAuthorizationDriverContract(harness, api) {
|
|
|
3626
3839
|
});
|
|
3627
3840
|
});
|
|
3628
3841
|
// Al cerrar el grupo: toda capacidad declarada `true` tiene que haber
|
|
3629
|
-
// registrado su par
|
|
3630
|
-
|
|
3842
|
+
// registrado su par — y las de `BOTH_FACES_REQUIRED`, también con `false`.
|
|
3843
|
+
// Es la regla "jamás un skip" hecha ejecutable.
|
|
3844
|
+
const uncovered = uncoveredCapabilities(harness.capabilities, covered);
|
|
3631
3845
|
if (uncovered.length) {
|
|
3632
|
-
throw new Error(`[contrato ${harness.name}] declara ${uncovered.map((c) => `'${c}:
|
|
3846
|
+
throw new Error(`[contrato ${harness.name}] declara ${uncovered.map((c) => `'${c}: ${String(harness.capabilities[c])}'`).join(', ')} ` +
|
|
3633
3847
|
`pero el contrato no tiene caso para ese valor todavía. Declara lo observable hoy.`);
|
|
3634
3848
|
}
|
|
3635
3849
|
}
|
|
3850
|
+
/**
|
|
3851
|
+
* La regla del guard, PURA (para que `contract_harness.spec.ts` la juzgue sin
|
|
3852
|
+
* borrar caras): una capacidad `true` sin caso registrado queda sin cubrir;
|
|
3853
|
+
* y una de `BOTH_FACES_REQUIRED` queda sin cubrir con CUALQUIER valor.
|
|
3854
|
+
*/
|
|
3855
|
+
export function uncoveredCapabilities(capabilities, covered) {
|
|
3856
|
+
return Object.keys(capabilities).filter((capability) => (capabilities[capability] || BOTH_FACES_REQUIRED.has(capability)) && !covered.has(capability));
|
|
3857
|
+
}
|
|
3858
|
+
/**
|
|
3859
|
+
* Capacidades cuyas DOS caras son obligatorias (L-2): declarar `false` sin
|
|
3860
|
+
* un caso `whenFalse` registrado lanza igual que declarar `true` sin
|
|
3861
|
+
* `whenTrue`. El guard general solo obliga a la cara `true` porque los pares
|
|
3862
|
+
* de nivel (`listDenies`, `purgeRole`…) no registran nada en un harness
|
|
3863
|
+
* `core` y `false` ahí es legítimo; `transactionalWrites` es composición del
|
|
3864
|
+
* manager y se observa en todos los niveles, así que su `false` sin caso es
|
|
3865
|
+
* un skip disfrazado — «rechazo activo» sería una etiqueta.
|
|
3866
|
+
*/
|
|
3867
|
+
const BOTH_FACES_REQUIRED = new Set(['transactionalWrites']);
|
|
3636
3868
|
//# sourceMappingURL=contract.js.map
|