@jantstack/adonis-authz 2.4.0-alpha.1 → 2.4.0-alpha.10
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 +496 -29
- package/build/commands/authz_catalog_diff.d.ts.map +1 -1
- package/build/commands/authz_catalog_diff.js +0 -1
- package/build/commands/authz_catalog_diff.js.map +1 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -1
- package/build/commands/authz_catalog_prune_orphans.js +0 -1
- package/build/commands/authz_catalog_prune_orphans.js.map +1 -1
- package/build/commands/authz_freeze.d.ts.map +1 -1
- package/build/commands/authz_freeze.js +1 -2
- package/build/commands/authz_freeze.js.map +1 -1
- package/build/commands/authz_reconcile.d.ts +15 -6
- package/build/commands/authz_reconcile.d.ts.map +1 -1
- package/build/commands/authz_reconcile.js +143 -20
- package/build/commands/authz_reconcile.js.map +1 -1
- package/build/commands/authz_relations_reconcile.d.ts +72 -1
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -1
- package/build/commands/authz_relations_reconcile.js +234 -21
- package/build/commands/authz_relations_reconcile.js.map +1 -1
- package/build/commands/authz_scopes_relay.d.ts +50 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -1
- package/build/commands/authz_scopes_relay.js +213 -10
- package/build/commands/authz_scopes_relay.js.map +1 -1
- package/build/commands/authz_scopes_requeue.d.ts +80 -0
- package/build/commands/authz_scopes_requeue.d.ts.map +1 -0
- package/build/commands/authz_scopes_requeue.js +230 -0
- package/build/commands/authz_scopes_requeue.js.map +1 -0
- package/build/commands/authz_unfreeze.d.ts +12 -4
- package/build/commands/authz_unfreeze.d.ts.map +1 -1
- package/build/commands/authz_unfreeze.js +74 -9
- package/build/commands/authz_unfreeze.js.map +1 -1
- package/build/commands/main.d.ts +16 -13
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +59 -11
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +49 -3
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +72 -7
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/index.d.ts +15 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +16 -3
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +7 -2
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +20 -3
- package/build/providers/authz_provider.js.map +1 -1
- package/build/src/define_config.d.ts +106 -1
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js +11 -0
- 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 +74 -9
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/database_relations_driver.js +391 -93
- package/build/src/drivers/database_relations_driver.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts +71 -10
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +195 -16
- 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 +72 -6
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_relations_driver.js +308 -50
- package/build/src/drivers/openfga_relations_driver.js.map +1 -1
- package/build/src/errors.d.ts +175 -4
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +219 -3
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +100 -20
- package/build/src/freeze.d.ts.map +1 -1
- package/build/src/freeze.js +159 -25
- package/build/src/freeze.js.map +1 -1
- package/build/src/manager.d.ts +239 -7
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +1192 -148
- 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 +51 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -1
- package/build/src/relations/define_relations_config.js +68 -1
- package/build/src/relations/define_relations_config.js.map +1 -1
- package/build/src/relations/manager.d.ts +106 -6
- package/build/src/relations/manager.d.ts.map +1 -1
- package/build/src/relations/manager.js +368 -30
- package/build/src/relations/manager.js.map +1 -1
- package/build/src/relations/reconcile.d.ts +81 -6
- package/build/src/relations/reconcile.d.ts.map +1 -1
- package/build/src/relations/reconcile.js +233 -77
- package/build/src/relations/reconcile.js.map +1 -1
- package/build/src/scope_outbox.d.ts +54 -0
- package/build/src/scope_outbox.d.ts.map +1 -1
- package/build/src/scope_outbox.js +274 -33
- 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/sql_expiry.d.ts +2 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -1
- package/build/src/shared/sql_expiry.js +4 -0
- package/build/src/shared/sql_expiry.js.map +1 -1
- package/build/src/shared/transaction_guard.d.ts +53 -0
- package/build/src/shared/transaction_guard.d.ts.map +1 -0
- package/build/src/shared/transaction_guard.js +74 -0
- package/build/src/shared/transaction_guard.js.map +1 -0
- package/build/src/sql_descendants.d.ts +8 -0
- package/build/src/sql_descendants.d.ts.map +1 -1
- package/build/src/sql_descendants.js.map +1 -1
- 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 +244 -9
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +6 -4
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +3 -2
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +9 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -1
- package/build/src/testing/migration_contract.js +5 -2
- package/build/src/testing/migration_contract.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 +603 -19
- 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/testing/scope_outbox_contract.d.ts +98 -0
- package/build/src/testing/scope_outbox_contract.d.ts.map +1 -0
- package/build/src/testing/scope_outbox_contract.js +677 -0
- package/build/src/testing/scope_outbox_contract.js.map +1 -0
- 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 +708 -38
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +55 -0
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/authorization.stub +45 -1
- package/build/stubs/migration.stub +20 -10
- package/package.json +1 -1
|
@@ -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.
|
|
@@ -2223,7 +2244,10 @@ export function registerAuthorizationDriverContract(harness, api) {
|
|
|
2223
2244
|
await driver.grant(alice, 'propio', unitA1);
|
|
2224
2245
|
await driver.grant(alice, 'nieto', unitA1x);
|
|
2225
2246
|
assert.isTrue(await driver.authorize(alice, 'docs:write', unitA1));
|
|
2226
|
-
|
|
2247
|
+
// Desde 2.4.0-alpha.5 `scopes.*` devuelven el RECIBO del encolado
|
|
2248
|
+
// (`{ queued }`); sin outbox, `{ queued: [] }` = aplicado en línea. Lo
|
|
2249
|
+
// que Z1 fija —ningún contador de lo purgado a medias— sigue en pie.
|
|
2250
|
+
assert.deepEqual(await authz.scopes.detached(unitA1), { queued: [] }, 'no devuelve nada que declarar a medias: el recibo vacío no es un contador de lo purgado');
|
|
2227
2251
|
assert.deepEqual(await driver.listRoles(alice, unitA1), [], 'los HECHOS del scope, purgados');
|
|
2228
2252
|
assert.isFalse(await driver.authorize(alice, 'docs:write', unitA1));
|
|
2229
2253
|
const view = await new CatalogCache().view();
|
|
@@ -3255,9 +3279,201 @@ export function registerAuthorizationDriverContract(harness, api) {
|
|
|
3255
3279
|
// `hierarchyFacts: false` → lo observan N1b/N2/N4/N5 (arriba): el driver
|
|
3256
3280
|
// responde según el árbol que le resuelve el consumidor, y `tree.move`
|
|
3257
3281
|
// cambia la respuesta sin escritura. `true` llega en Fase 3b.
|
|
3258
|
-
// `injectableClock` tiene su par arriba (2.5 · J1). `
|
|
3259
|
-
//
|
|
3260
|
-
//
|
|
3282
|
+
// `injectableClock` tiene su par arriba (2.5 · J1). `transactionalWrites`
|
|
3283
|
+
// (L-2) tiene su par al final: la cara `false` hoy, la `true` con L-3.
|
|
3284
|
+
// ── L-2 · `transactionalWrites` (panel `{trx}`, (C)) ──────────────────
|
|
3285
|
+
// Las DOS caras son obligatorias en este par (`BOTH_FACES_REQUIRED`): una
|
|
3286
|
+
// `whenFalse` ausente se saltaba en silencio y «rechazo activo» sería
|
|
3287
|
+
// una etiqueta. Lo que la cara `false` fija es la PUERTA 1 (por llamada,
|
|
3288
|
+
// siempre activa) y la PUERTA 2 (por config, opt-in), las dos con el
|
|
3289
|
+
// manager construido sobre el driver del harness, en TODOS los niveles:
|
|
3290
|
+
// son composición del manager sobre `driver.capabilities`, así que un
|
|
3291
|
+
// driver de terceros de nivel `core` también las observa.
|
|
3292
|
+
caseFor('transactionalWrites', {
|
|
3293
|
+
// L-3: la cara `true`. Lo que se juzga es «los dos o ninguno con TU
|
|
3294
|
+
// transacción» POR CENSO: el rollback deja CERO filas nuevas (no solo
|
|
3295
|
+
// `authorize` false — una respuesta puede salir bien con la fila viva),
|
|
3296
|
+
// el commit las deja escritas, y mientras la transacción está abierta
|
|
3297
|
+
// la autoridad (que lee por la conexión del motor) no ve nada. Y la
|
|
3298
|
+
// otra mitad de la regla: una transacción que NO es la del driver no
|
|
3299
|
+
// recibe ni una sentencia. Cuatro escrituras, las dos caras cada una.
|
|
3300
|
+
whenTrue: () => {
|
|
3301
|
+
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, }) => {
|
|
3302
|
+
assert.equal(driver.capabilities?.transactionalWrites, true, 'el harness declara transactionalWrites: true y el driver no: declara lo observable');
|
|
3303
|
+
const tx = harness.transactions ?? lucidContractTransactions();
|
|
3304
|
+
const alice = subject();
|
|
3305
|
+
const org = await orgUnder(tree, APP_SCOPE);
|
|
3306
|
+
const authz = managerOver({});
|
|
3307
|
+
const can = () => driver.authorize(alice, 'docs:read', org);
|
|
3308
|
+
const census = () => tx.census(alice);
|
|
3309
|
+
// Una transacción que se queda abierta tras una aserción fallida
|
|
3310
|
+
// retiene una conexión del pool y arrastra a los casos siguientes:
|
|
3311
|
+
// el rollback va en `finally`, y el commit solo si nada falló.
|
|
3312
|
+
const rolledBack = async (fn) => {
|
|
3313
|
+
const t = await tx.begin();
|
|
3314
|
+
try {
|
|
3315
|
+
await fn(t.client);
|
|
3316
|
+
}
|
|
3317
|
+
finally {
|
|
3318
|
+
await t.rollback();
|
|
3319
|
+
}
|
|
3320
|
+
};
|
|
3321
|
+
const committed = async (fn) => {
|
|
3322
|
+
const t = await tx.begin();
|
|
3323
|
+
try {
|
|
3324
|
+
await fn(t.client);
|
|
3325
|
+
}
|
|
3326
|
+
catch (error) {
|
|
3327
|
+
await t.rollback();
|
|
3328
|
+
throw error;
|
|
3329
|
+
}
|
|
3330
|
+
await t.commit();
|
|
3331
|
+
};
|
|
3332
|
+
// grant: dentro no se ve desde fuera; rollback ⇒ nada; commit ⇒ concede.
|
|
3333
|
+
let seenInside;
|
|
3334
|
+
await rolledBack(async (transaction) => {
|
|
3335
|
+
await authz.grant(alice, 'org-editor', org, { transaction });
|
|
3336
|
+
seenInside = await can();
|
|
3337
|
+
});
|
|
3338
|
+
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)');
|
|
3339
|
+
assert.isFalse(await can(), 'grant + rollback ⇒ no concede');
|
|
3340
|
+
assert.isFalse(seenInside, 'la autoridad lee por la conexión del motor: el grant sin confirmar no concede');
|
|
3341
|
+
await committed(async (transaction) => {
|
|
3342
|
+
await authz.grant(alice, 'org-editor', org, { transaction });
|
|
3343
|
+
});
|
|
3344
|
+
assert.isTrue(await can(), 'grant + commit ⇒ concede');
|
|
3345
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 });
|
|
3346
|
+
// revoke: rollback ⇒ la asignación sigue; commit ⇒ se va.
|
|
3347
|
+
await rolledBack(async (transaction) => {
|
|
3348
|
+
await authz.revoke(alice, 'org-editor', org, { transaction });
|
|
3349
|
+
seenInside = await can();
|
|
3350
|
+
});
|
|
3351
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 }, 'ROJO: el revoke sobrevivió al rollback');
|
|
3352
|
+
assert.isTrue(await can(), 'revoke + rollback ⇒ sigue concediendo');
|
|
3353
|
+
assert.isTrue(seenInside, 'el revoke sin confirmar no se ve desde la conexión del motor');
|
|
3354
|
+
await committed(async (transaction) => {
|
|
3355
|
+
await authz.revoke(alice, 'org-editor', org, { transaction });
|
|
3356
|
+
});
|
|
3357
|
+
assert.isFalse(await can(), 'revoke + commit ⇒ no concede');
|
|
3358
|
+
assert.deepEqual(await census(), { assignments: 0, denies: 0 });
|
|
3359
|
+
// deny (sobre un grant confirmado): rollback ⇒ sigue concediendo; commit ⇒ deniega.
|
|
3360
|
+
await authz.grant(alice, 'org-editor', org);
|
|
3361
|
+
await rolledBack(async (transaction) => {
|
|
3362
|
+
await authz.deny(alice, 'docs:read', org, { transaction });
|
|
3363
|
+
seenInside = await can();
|
|
3364
|
+
});
|
|
3365
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 }, 'ROJO: el deny sobrevivió al rollback');
|
|
3366
|
+
assert.isTrue(await can(), 'deny + rollback ⇒ sigue concediendo');
|
|
3367
|
+
assert.isTrue(seenInside, 'el deny sin confirmar no se ve desde la conexión del motor');
|
|
3368
|
+
await committed(async (transaction) => {
|
|
3369
|
+
await authz.deny(alice, 'docs:read', org, { transaction });
|
|
3370
|
+
});
|
|
3371
|
+
assert.isFalse(await can(), 'deny + commit ⇒ deniega');
|
|
3372
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 1 });
|
|
3373
|
+
// removeDeny: rollback ⇒ el deny sigue; commit ⇒ restaura.
|
|
3374
|
+
await rolledBack(async (transaction) => {
|
|
3375
|
+
await authz.removeDeny(alice, 'docs:read', org, { transaction });
|
|
3376
|
+
seenInside = await can();
|
|
3377
|
+
});
|
|
3378
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 1 }, 'ROJO: el removeDeny sobrevivió al rollback');
|
|
3379
|
+
assert.isFalse(await can(), 'removeDeny + rollback ⇒ sigue denegando');
|
|
3380
|
+
assert.isFalse(seenInside, 'el removeDeny sin confirmar no se ve desde la conexión del motor');
|
|
3381
|
+
await committed(async (transaction) => {
|
|
3382
|
+
await authz.removeDeny(alice, 'docs:read', org, { transaction });
|
|
3383
|
+
});
|
|
3384
|
+
assert.isTrue(await can(), 'removeDeny + commit ⇒ restaura');
|
|
3385
|
+
assert.deepEqual(await census(), { assignments: 1, denies: 0 });
|
|
3386
|
+
// Una transacción que NO es la del driver (`assertCallerTransaction`):
|
|
3387
|
+
// 500 `E_AUTHZ_CONFIG` y ni una sentencia por ella (espía).
|
|
3388
|
+
const statements = [];
|
|
3389
|
+
const spy = (shape) => new Proxy(shape, {
|
|
3390
|
+
get(target, prop) {
|
|
3391
|
+
if (prop === 'from' || prop === 'table' || prop === 'raw' || prop === 'query' || prop === 'knexQuery') {
|
|
3392
|
+
return (...args) => {
|
|
3393
|
+
statements.push(`${String(prop)}(${String(args[0] ?? '')})`);
|
|
3394
|
+
return { where: () => ({}), select: async () => [], insert: async () => [], delete: async () => 0 };
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
return Reflect.get(target, prop);
|
|
3398
|
+
},
|
|
3399
|
+
});
|
|
3400
|
+
const foreign = spy({ isTransaction: true, connectionName: `not-${harness.name}-${uuidv7()}` });
|
|
3401
|
+
const notATransaction = spy({ isTransaction: false, connectionName: 'primary' });
|
|
3402
|
+
const wholeDb = spy({ connection() { }, primaryConnectionName: 'primary' });
|
|
3403
|
+
const bob = subject();
|
|
3404
|
+
for (const [label, transaction] of [
|
|
3405
|
+
['otra conexión', foreign],
|
|
3406
|
+
['un cliente sin transacción', notATransaction],
|
|
3407
|
+
['el db entero', wholeDb],
|
|
3408
|
+
]) {
|
|
3409
|
+
for (const [operation, run] of [
|
|
3410
|
+
['grant', () => authz.grant(bob, 'org-editor', org, { transaction })],
|
|
3411
|
+
['revoke', () => authz.revoke(bob, 'org-editor', org, { transaction })],
|
|
3412
|
+
['deny', () => authz.deny(bob, 'docs:read', org, { transaction })],
|
|
3413
|
+
['removeDeny', () => authz.removeDeny(bob, 'docs:read', org, { transaction })],
|
|
3414
|
+
]) {
|
|
3415
|
+
const error = await rejectsWith(assert, run, { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3416
|
+
assert.include(error.message, operation, `${label}/${operation}: nombra la operación`);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
assert.deepEqual(statements, [], 'ni una sentencia por una transacción que no es la del driver');
|
|
3420
|
+
assert.deepEqual(await tx.census(bob), { assignments: 0, denies: 0 });
|
|
3421
|
+
assert.isFalse(await driver.authorize(bob, 'docs:read', org));
|
|
3422
|
+
})?.timeout(30_000);
|
|
3423
|
+
},
|
|
3424
|
+
whenFalse: () => {
|
|
3425
|
+
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, }) => {
|
|
3426
|
+
// El harness declara `false`: el driver tiene que decir lo mismo
|
|
3427
|
+
// (el manager lee `driver.capabilities`, no el harness).
|
|
3428
|
+
assert.notEqual(driver.capabilities?.transactionalWrites, true, 'el harness declara transactionalWrites: false y el driver declara true: declara lo observable');
|
|
3429
|
+
const touched = [];
|
|
3430
|
+
const spied = new Proxy(driver, {
|
|
3431
|
+
get(target, prop, receiver) {
|
|
3432
|
+
const value = Reflect.get(target, prop, receiver);
|
|
3433
|
+
if (typeof value === 'function' && typeof prop === 'string') {
|
|
3434
|
+
return (...args) => {
|
|
3435
|
+
touched.push(prop);
|
|
3436
|
+
return value.apply(receiver, args);
|
|
3437
|
+
};
|
|
3438
|
+
}
|
|
3439
|
+
return value;
|
|
3440
|
+
},
|
|
3441
|
+
});
|
|
3442
|
+
// La forma de un `TransactionClientContract` de Lucid: la puerta
|
|
3443
|
+
// decide por la CAPACIDAD, antes de mirar qué transacción es.
|
|
3444
|
+
const trx = { from() { }, table() { }, isTransaction: true, connectionName: 'primary' };
|
|
3445
|
+
const alice = subject();
|
|
3446
|
+
const org = await orgUnder(tree, APP_SCOPE);
|
|
3447
|
+
const authz = managerOver({}, spied);
|
|
3448
|
+
const writes = [
|
|
3449
|
+
['grant', () => authz.grant(alice, 'org-editor', org, { transaction: trx })],
|
|
3450
|
+
['revoke', () => authz.revoke(alice, 'org-editor', org, { transaction: trx })],
|
|
3451
|
+
['deny', () => authz.deny(alice, 'docs:read', org, { transaction: trx })],
|
|
3452
|
+
['removeDeny', () => authz.removeDeny(alice, 'docs:read', org, { transaction: trx })],
|
|
3453
|
+
];
|
|
3454
|
+
for (const [operation, run] of writes) {
|
|
3455
|
+
const error = await rejectsWith(assert, run, { status: 500, code: 'E_AUTHZ_UNSUPPORTED' });
|
|
3456
|
+
assert.include(error.message, `'${harness.name}'`, `${operation}: nombra el driver`);
|
|
3457
|
+
assert.include(error.message, operation, `${operation}: nombra la operación`);
|
|
3458
|
+
assert.include(error.message, 'requireTransactionalWrites', `${operation}: la letra lleva la salida`);
|
|
3459
|
+
}
|
|
3460
|
+
assert.deepEqual(touched, [], 'cero llamadas al driver: el rechazo va ANTES del backend');
|
|
3461
|
+
// Sin `{ transaction }` la MISMA escritura entra: la puerta es del
|
|
3462
|
+
// parámetro, no del driver.
|
|
3463
|
+
await authz.grant(alice, 'org-editor', org);
|
|
3464
|
+
assert.isTrue(await driver.authorize(alice, 'docs:read', org));
|
|
3465
|
+
assert.include(touched, 'grant');
|
|
3466
|
+
// Puerta 2, opt-in: quien quiera fallar al ARRANCAR lo pide, y
|
|
3467
|
+
// entonces el driver ni se resuelve (tampoco para leer).
|
|
3468
|
+
const strict = managerOver({ requireTransactionalWrites: true }, spied);
|
|
3469
|
+
const atResolve = await rejectsWith(assert, () => strict.driver(), { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3470
|
+
assert.include(atResolve.message, `'${harness.name}'`);
|
|
3471
|
+
assert.include(atResolve.message, 'transactionalWrites');
|
|
3472
|
+
await rejectsWith(assert, () => strict.authorize(alice, 'docs:read', org), { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3473
|
+
await rejectsWith(assert, () => strict.grant(alice, 'org-editor', org), { status: 500, code: 'E_AUTHZ_CONFIG' });
|
|
3474
|
+
});
|
|
3475
|
+
},
|
|
3476
|
+
});
|
|
3261
3477
|
// ── 3b-2e · E6 · `singleCheckAuthorize` ──────────────────────────────
|
|
3262
3478
|
caseFor('singleCheckAuthorize', {
|
|
3263
3479
|
whenTrue: () => {
|
|
@@ -3626,11 +3842,30 @@ export function registerAuthorizationDriverContract(harness, api) {
|
|
|
3626
3842
|
});
|
|
3627
3843
|
});
|
|
3628
3844
|
// Al cerrar el grupo: toda capacidad declarada `true` tiene que haber
|
|
3629
|
-
// registrado su par
|
|
3630
|
-
|
|
3845
|
+
// registrado su par — y las de `BOTH_FACES_REQUIRED`, también con `false`.
|
|
3846
|
+
// Es la regla "jamás un skip" hecha ejecutable.
|
|
3847
|
+
const uncovered = uncoveredCapabilities(harness.capabilities, covered);
|
|
3631
3848
|
if (uncovered.length) {
|
|
3632
|
-
throw new Error(`[contrato ${harness.name}] declara ${uncovered.map((c) => `'${c}:
|
|
3849
|
+
throw new Error(`[contrato ${harness.name}] declara ${uncovered.map((c) => `'${c}: ${String(harness.capabilities[c])}'`).join(', ')} ` +
|
|
3633
3850
|
`pero el contrato no tiene caso para ese valor todavía. Declara lo observable hoy.`);
|
|
3634
3851
|
}
|
|
3635
3852
|
}
|
|
3853
|
+
/**
|
|
3854
|
+
* La regla del guard, PURA (para que `contract_harness.spec.ts` la juzgue sin
|
|
3855
|
+
* borrar caras): una capacidad `true` sin caso registrado queda sin cubrir;
|
|
3856
|
+
* y una de `BOTH_FACES_REQUIRED` queda sin cubrir con CUALQUIER valor.
|
|
3857
|
+
*/
|
|
3858
|
+
export function uncoveredCapabilities(capabilities, covered) {
|
|
3859
|
+
return Object.keys(capabilities).filter((capability) => (capabilities[capability] || BOTH_FACES_REQUIRED.has(capability)) && !covered.has(capability));
|
|
3860
|
+
}
|
|
3861
|
+
/**
|
|
3862
|
+
* Capacidades cuyas DOS caras son obligatorias (L-2): declarar `false` sin
|
|
3863
|
+
* un caso `whenFalse` registrado lanza igual que declarar `true` sin
|
|
3864
|
+
* `whenTrue`. El guard general solo obliga a la cara `true` porque los pares
|
|
3865
|
+
* de nivel (`listDenies`, `purgeRole`…) no registran nada en un harness
|
|
3866
|
+
* `core` y `false` ahí es legítimo; `transactionalWrites` es composición del
|
|
3867
|
+
* manager y se observa en todos los niveles, así que su `false` sin caso es
|
|
3868
|
+
* un skip disfrazado — «rechazo activo» sería una etiqueta.
|
|
3869
|
+
*/
|
|
3870
|
+
const BOTH_FACES_REQUIRED = new Set(['transactionalWrites']);
|
|
3636
3871
|
//# sourceMappingURL=contract.js.map
|