@jantstack/adonis-authz 2.5.0-alpha.2 → 2.5.0-alpha.4
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 +743 -28
- package/build/commands/authz_catalog_diff.d.ts +0 -1
- package/build/commands/authz_catalog_diff.js +0 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts +0 -1
- package/build/commands/authz_catalog_prune_orphans.js +0 -1
- package/build/commands/authz_catalog_sync.d.ts +0 -1
- package/build/commands/authz_catalog_sync.js +0 -1
- package/build/commands/authz_freeze.d.ts +0 -1
- package/build/commands/authz_freeze.js +0 -1
- package/build/commands/authz_reconcile.d.ts +0 -1
- package/build/commands/authz_reconcile.js +0 -1
- package/build/commands/authz_relations_reconcile.d.ts +6 -1
- package/build/commands/authz_relations_reconcile.js +163 -5
- package/build/commands/authz_scopes_relay.d.ts +0 -1
- package/build/commands/authz_scopes_relay.js +49 -5
- package/build/commands/authz_scopes_requeue.d.ts +0 -1
- package/build/commands/authz_scopes_requeue.js +14 -2
- package/build/commands/authz_unfreeze.d.ts +0 -1
- package/build/commands/authz_unfreeze.js +0 -1
- package/build/commands/main.d.ts +0 -1
- package/build/commands/main.js +0 -1
- package/build/commands/openfga_provision.d.ts +72 -1
- package/build/commands/openfga_provision.js +236 -4
- package/build/configure.d.ts +0 -1
- package/build/configure.js +0 -1
- package/build/index.d.ts +11 -9
- package/build/index.js +12 -5
- package/build/providers/authz_provider.d.ts +0 -1
- package/build/providers/authz_provider.js +0 -1
- package/build/services/main.d.ts +0 -1
- package/build/services/main.js +0 -1
- package/build/services/relations.d.ts +0 -1
- package/build/services/relations.js +0 -1
- package/build/src/catalog/catalog.d.ts +0 -1
- package/build/src/catalog/catalog.js +0 -1
- package/build/src/catalog/catalog_cache.d.ts +13 -1
- package/build/src/catalog/catalog_cache.js +25 -1
- package/build/src/clock.d.ts +0 -1
- package/build/src/clock.js +0 -1
- package/build/src/define_config.d.ts +0 -1
- package/build/src/define_config.js +24 -1
- package/build/src/drivers/database_driver.d.ts +6 -1
- package/build/src/drivers/database_driver.js +22 -2
- package/build/src/drivers/database_relations_driver.d.ts +46 -5
- package/build/src/drivers/database_relations_driver.js +297 -59
- package/build/src/drivers/openfga_driver.d.ts +159 -26
- package/build/src/drivers/openfga_driver.js +980 -231
- package/build/src/drivers/openfga_facts.d.ts +112 -1
- package/build/src/drivers/openfga_facts.js +351 -11
- package/build/src/drivers/openfga_relations_driver.d.ts +109 -8
- package/build/src/drivers/openfga_relations_driver.js +817 -53
- package/build/src/errors.d.ts +233 -1
- package/build/src/errors.js +283 -1
- package/build/src/expiry.d.ts +51 -2
- package/build/src/expiry.js +96 -2
- package/build/src/freeze.d.ts +0 -1
- package/build/src/freeze.js +0 -1
- package/build/src/hierarchical_resolver.d.ts +0 -1
- package/build/src/hierarchical_resolver.js +0 -1
- package/build/src/http/app_access_middleware.d.ts +0 -1
- package/build/src/http/app_access_middleware.js +0 -1
- package/build/src/http/resource_access_middleware.d.ts +0 -1
- package/build/src/http/resource_access_middleware.js +0 -1
- package/build/src/identity.d.ts +2 -1
- package/build/src/identity.js +4 -1
- package/build/src/manager.d.ts +0 -1
- package/build/src/manager.js +164 -31
- package/build/src/memoize_ancestors.d.ts +0 -1
- package/build/src/memoize_ancestors.js +0 -1
- package/build/src/models/authz_assignment.d.ts +5 -6
- package/build/src/models/authz_assignment.js +0 -1
- package/build/src/models/authz_deny.d.ts +5 -6
- package/build/src/models/authz_deny.js +0 -1
- package/build/src/models/authz_permission.d.ts +5 -6
- package/build/src/models/authz_permission.js +0 -1
- package/build/src/models/authz_role.d.ts +5 -6
- package/build/src/models/authz_role.js +0 -1
- package/build/src/models/authz_role_permission.d.ts +5 -6
- package/build/src/models/authz_role_permission.js +0 -1
- package/build/src/openfga.d.ts +3 -4
- package/build/src/openfga.js +5 -3
- package/build/src/reconcile.d.ts +0 -1
- package/build/src/reconcile.js +0 -1
- package/build/src/relation_partition_trigger.d.ts +0 -1
- package/build/src/relation_partition_trigger.js +0 -1
- package/build/src/relations/check_many.d.ts +1 -2
- package/build/src/relations/check_many.js +6 -5
- package/build/src/relations/define_relations_config.d.ts +140 -5
- package/build/src/relations/define_relations_config.js +205 -6
- package/build/src/relations/deny_reach.d.ts +102 -0
- package/build/src/relations/deny_reach.js +230 -0
- package/build/src/relations/group_depth.d.ts +215 -0
- package/build/src/relations/group_depth.js +453 -0
- package/build/src/relations/manager.d.ts +46 -8
- package/build/src/relations/manager.js +178 -16
- package/build/src/relations/page.d.ts +19 -0
- package/build/src/relations/page.js +74 -0
- package/build/src/relations/reconcile.d.ts +274 -2
- package/build/src/relations/reconcile.js +1006 -162
- package/build/src/relations/tuple_key.d.ts +0 -1
- package/build/src/relations/tuple_key.js +0 -1
- package/build/src/relations/tuple_order.d.ts +127 -0
- package/build/src/relations/tuple_order.js +599 -0
- package/build/src/relations_config_store.d.ts +57 -2
- package/build/src/relations_config_store.js +155 -5
- package/build/src/relations_reconcile.d.ts +0 -1
- package/build/src/relations_reconcile.js +0 -1
- package/build/src/scope_outbox.d.ts +0 -1
- package/build/src/scope_outbox.js +0 -1
- package/build/src/shared/backend_guard.d.ts +60 -1
- package/build/src/shared/backend_guard.js +179 -2
- package/build/src/shared/sql_expiry.d.ts +0 -1
- package/build/src/shared/sql_expiry.js +0 -1
- package/build/src/shared/transaction_guard.d.ts +0 -1
- package/build/src/shared/transaction_guard.js +0 -1
- package/build/src/sql_descendants.d.ts +0 -1
- package/build/src/sql_descendants.js +0 -1
- package/build/src/testing/contract.d.ts +7 -1
- package/build/src/testing/contract.js +77 -1
- package/build/src/testing/main.d.ts +1 -2
- package/build/src/testing/main.js +1 -2
- package/build/src/testing/migration_contract.d.ts +0 -1
- package/build/src/testing/migration_contract.js +0 -1
- package/build/src/testing/relations_contract.d.ts +25 -1
- package/build/src/testing/relations_contract.js +996 -49
- package/build/src/testing/relations_reconcile_contract.d.ts +9 -1
- package/build/src/testing/relations_reconcile_contract.js +207 -1
- package/build/src/testing/scope_outbox_contract.d.ts +0 -1
- package/build/src/testing/scope_outbox_contract.js +0 -1
- package/build/src/testing/scope_tree.d.ts +0 -1
- package/build/src/testing/scope_tree.js +0 -1
- package/build/src/testing/test_clock.d.ts +0 -1
- package/build/src/testing/test_clock.js +0 -1
- package/build/src/traits/authz_scopes.d.ts +0 -1
- package/build/src/traits/authz_scopes.js +0 -1
- package/build/src/traits/has_uuid.d.ts +5 -6
- package/build/src/traits/has_uuid.js +0 -1
- package/build/src/types.d.ts +170 -15
- package/build/src/types.js +6 -1
- package/build/stubs/config/authorization.stub +22 -6
- package/build/stubs/main.d.ts +0 -1
- package/build/stubs/main.js +0 -1
- package/package.json +4 -3
- package/build/commands/authz_catalog_diff.d.ts.map +0 -1
- package/build/commands/authz_catalog_diff.js.map +0 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +0 -1
- package/build/commands/authz_catalog_prune_orphans.js.map +0 -1
- package/build/commands/authz_catalog_sync.d.ts.map +0 -1
- package/build/commands/authz_catalog_sync.js.map +0 -1
- package/build/commands/authz_freeze.d.ts.map +0 -1
- package/build/commands/authz_freeze.js.map +0 -1
- package/build/commands/authz_reconcile.d.ts.map +0 -1
- package/build/commands/authz_reconcile.js.map +0 -1
- package/build/commands/authz_relations_reconcile.d.ts.map +0 -1
- package/build/commands/authz_relations_reconcile.js.map +0 -1
- package/build/commands/authz_scopes_relay.d.ts.map +0 -1
- package/build/commands/authz_scopes_relay.js.map +0 -1
- package/build/commands/authz_scopes_requeue.d.ts.map +0 -1
- package/build/commands/authz_scopes_requeue.js.map +0 -1
- package/build/commands/authz_unfreeze.d.ts.map +0 -1
- package/build/commands/authz_unfreeze.js.map +0 -1
- package/build/commands/main.d.ts.map +0 -1
- package/build/commands/main.js.map +0 -1
- package/build/commands/openfga_provision.d.ts.map +0 -1
- package/build/commands/openfga_provision.js.map +0 -1
- package/build/configure.d.ts.map +0 -1
- package/build/configure.js.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/index.js.map +0 -1
- package/build/providers/authz_provider.d.ts.map +0 -1
- package/build/providers/authz_provider.js.map +0 -1
- package/build/services/main.d.ts.map +0 -1
- package/build/services/main.js.map +0 -1
- package/build/services/relations.d.ts.map +0 -1
- package/build/services/relations.js.map +0 -1
- package/build/src/catalog/catalog.d.ts.map +0 -1
- package/build/src/catalog/catalog.js.map +0 -1
- package/build/src/catalog/catalog_cache.d.ts.map +0 -1
- package/build/src/catalog/catalog_cache.js.map +0 -1
- package/build/src/clock.d.ts.map +0 -1
- package/build/src/clock.js.map +0 -1
- package/build/src/define_config.d.ts.map +0 -1
- package/build/src/define_config.js.map +0 -1
- package/build/src/drivers/database_driver.d.ts.map +0 -1
- package/build/src/drivers/database_driver.js.map +0 -1
- package/build/src/drivers/database_relations_driver.d.ts.map +0 -1
- package/build/src/drivers/database_relations_driver.js.map +0 -1
- package/build/src/drivers/openfga_driver.d.ts.map +0 -1
- package/build/src/drivers/openfga_driver.js.map +0 -1
- package/build/src/drivers/openfga_facts.d.ts.map +0 -1
- package/build/src/drivers/openfga_facts.js.map +0 -1
- package/build/src/drivers/openfga_relations_driver.d.ts.map +0 -1
- package/build/src/drivers/openfga_relations_driver.js.map +0 -1
- package/build/src/errors.d.ts.map +0 -1
- package/build/src/errors.js.map +0 -1
- package/build/src/expiry.d.ts.map +0 -1
- package/build/src/expiry.js.map +0 -1
- package/build/src/freeze.d.ts.map +0 -1
- package/build/src/freeze.js.map +0 -1
- package/build/src/hierarchical_resolver.d.ts.map +0 -1
- package/build/src/hierarchical_resolver.js.map +0 -1
- package/build/src/http/app_access_middleware.d.ts.map +0 -1
- package/build/src/http/app_access_middleware.js.map +0 -1
- package/build/src/http/resource_access_middleware.d.ts.map +0 -1
- package/build/src/http/resource_access_middleware.js.map +0 -1
- package/build/src/identity.d.ts.map +0 -1
- package/build/src/identity.js.map +0 -1
- package/build/src/manager.d.ts.map +0 -1
- package/build/src/manager.js.map +0 -1
- package/build/src/memoize_ancestors.d.ts.map +0 -1
- package/build/src/memoize_ancestors.js.map +0 -1
- package/build/src/models/authz_assignment.d.ts.map +0 -1
- package/build/src/models/authz_assignment.js.map +0 -1
- package/build/src/models/authz_deny.d.ts.map +0 -1
- package/build/src/models/authz_deny.js.map +0 -1
- package/build/src/models/authz_permission.d.ts.map +0 -1
- package/build/src/models/authz_permission.js.map +0 -1
- package/build/src/models/authz_role.d.ts.map +0 -1
- package/build/src/models/authz_role.js.map +0 -1
- package/build/src/models/authz_role_permission.d.ts.map +0 -1
- package/build/src/models/authz_role_permission.js.map +0 -1
- package/build/src/openfga.d.ts.map +0 -1
- package/build/src/openfga.js.map +0 -1
- package/build/src/reconcile.d.ts.map +0 -1
- package/build/src/reconcile.js.map +0 -1
- package/build/src/relation_partition_trigger.d.ts.map +0 -1
- package/build/src/relation_partition_trigger.js.map +0 -1
- package/build/src/relations/check_many.d.ts.map +0 -1
- package/build/src/relations/check_many.js.map +0 -1
- package/build/src/relations/define_relations_config.d.ts.map +0 -1
- package/build/src/relations/define_relations_config.js.map +0 -1
- package/build/src/relations/manager.d.ts.map +0 -1
- package/build/src/relations/manager.js.map +0 -1
- package/build/src/relations/reconcile.d.ts.map +0 -1
- package/build/src/relations/reconcile.js.map +0 -1
- package/build/src/relations/tuple_key.d.ts.map +0 -1
- package/build/src/relations/tuple_key.js.map +0 -1
- package/build/src/relations_config_store.d.ts.map +0 -1
- package/build/src/relations_config_store.js.map +0 -1
- package/build/src/relations_reconcile.d.ts.map +0 -1
- package/build/src/relations_reconcile.js.map +0 -1
- package/build/src/scope_outbox.d.ts.map +0 -1
- package/build/src/scope_outbox.js.map +0 -1
- package/build/src/shared/backend_guard.d.ts.map +0 -1
- package/build/src/shared/backend_guard.js.map +0 -1
- package/build/src/shared/sql_expiry.d.ts.map +0 -1
- package/build/src/shared/sql_expiry.js.map +0 -1
- package/build/src/shared/transaction_guard.d.ts.map +0 -1
- package/build/src/shared/transaction_guard.js.map +0 -1
- package/build/src/sql_descendants.d.ts.map +0 -1
- package/build/src/sql_descendants.js.map +0 -1
- package/build/src/testing/contract.d.ts.map +0 -1
- package/build/src/testing/contract.js.map +0 -1
- package/build/src/testing/main.d.ts.map +0 -1
- package/build/src/testing/main.js.map +0 -1
- package/build/src/testing/migration_contract.d.ts.map +0 -1
- package/build/src/testing/migration_contract.js.map +0 -1
- package/build/src/testing/relations_contract.d.ts.map +0 -1
- package/build/src/testing/relations_contract.js.map +0 -1
- package/build/src/testing/relations_reconcile_contract.d.ts.map +0 -1
- package/build/src/testing/relations_reconcile_contract.js.map +0 -1
- package/build/src/testing/scope_outbox_contract.d.ts.map +0 -1
- package/build/src/testing/scope_outbox_contract.js.map +0 -1
- package/build/src/testing/scope_tree.d.ts.map +0 -1
- package/build/src/testing/scope_tree.js.map +0 -1
- package/build/src/testing/test_clock.d.ts.map +0 -1
- package/build/src/testing/test_clock.js.map +0 -1
- package/build/src/traits/authz_scopes.d.ts.map +0 -1
- package/build/src/traits/authz_scopes.js.map +0 -1
- package/build/src/traits/has_uuid.d.ts.map +0 -1
- package/build/src/traits/has_uuid.js.map +0 -1
- package/build/src/types.d.ts.map +0 -1
- package/build/src/types.js.map +0 -1
- package/build/stubs/main.d.ts.map +0 -1
- package/build/stubs/main.js.map +0 -1
|
@@ -26,8 +26,9 @@ import { assertPartition, assertSubject, assertRelationId, assertExpiresAt, scop
|
|
|
26
26
|
import { isClock, systemClock } from '../clock.js';
|
|
27
27
|
import { assertNotFrozenRow } from '../freeze.js';
|
|
28
28
|
import { isRelUserset, normalizeRelationsCapabilities } from '../types.js';
|
|
29
|
-
import { assertRelationDeclared, assertObjectTypeDeclared, assertUsersetSubject } from './define_relations_config.js';
|
|
29
|
+
import { assertRelationDeclared, assertObjectTypeDeclared, assertUsersetSubject, assertRelationDenyable, assertDenySubject, assertDenyNoExpiry, } from './define_relations_config.js';
|
|
30
30
|
import { assertCheckManyObjects } from './check_many.js';
|
|
31
|
+
import { assertEnumeratePage, assertNoListPage } from './page.js';
|
|
31
32
|
/**
|
|
32
33
|
* ¿Cada POSICIÓN del array es un boolean? Por índice, nunca con `some`/`every`
|
|
33
34
|
* (que saltan los huecos de un array disperso: 🟡 A3 del auditor del lote A).
|
|
@@ -39,6 +40,27 @@ function everyPositionBoolean(answers) {
|
|
|
39
40
|
}
|
|
40
41
|
return true;
|
|
41
42
|
}
|
|
43
|
+
const DECISIONS = new Set(['allow', 'deny', 'none']);
|
|
44
|
+
/** ¿Cada POSICIÓN es una decisión (`'allow'|'deny'|'none'`)? Por índice (los huecos de un array disperso cuentan). */
|
|
45
|
+
function everyPositionDecision(answers) {
|
|
46
|
+
for (let i = 0; i < answers.length; i++) {
|
|
47
|
+
if (!DECISIONS.has(answers[i]))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/** Descripción corta de una respuesta inválida de `decideMany`, para el mensaje del 500. */
|
|
53
|
+
function describeDecisions(answer) {
|
|
54
|
+
if (!Array.isArray(answer))
|
|
55
|
+
return `${answer === null ? 'null' : typeof answer} (no es un array)`;
|
|
56
|
+
for (let i = 0; i < answer.length; i++) {
|
|
57
|
+
if (!(i in answer))
|
|
58
|
+
return `un array de ${answer.length} con un HUECO en la posición ${i}`;
|
|
59
|
+
if (!DECISIONS.has(answer[i]))
|
|
60
|
+
return `un array de ${answer.length} con ${JSON.stringify(answer[i])} en la posición ${i}`;
|
|
61
|
+
}
|
|
62
|
+
return `un array de ${answer.length}`;
|
|
63
|
+
}
|
|
42
64
|
/** Descripción corta de una respuesta inválida de `checkMany`, para el mensaje del 500. */
|
|
43
65
|
function describeManyAnswer(answer) {
|
|
44
66
|
if (!Array.isArray(answer))
|
|
@@ -120,7 +142,7 @@ export class RelationsManager {
|
|
|
120
142
|
* la escritura no ocurrió y se propaga SIN evento. Con `{ transaction }` el
|
|
121
143
|
* evento lleva además `transactional: true` (el rollback del llamante
|
|
122
144
|
* determina el resultado, pero es suyo y el paquete no lo ve). Envuelve las
|
|
123
|
-
*
|
|
145
|
+
* SEIS escrituras (desde el lote D también `deny`/`removeDeny`).
|
|
124
146
|
*
|
|
125
147
|
* **Y una escritura PARCIAL también es indeterminada** (cierre del 🟡 6 del
|
|
126
148
|
* auditor de alpha.3): la purga de `openfga` es multi-request (ortografías
|
|
@@ -306,7 +328,8 @@ export class RelationsManager {
|
|
|
306
328
|
timeoutMs: this.#options.freezeTimeoutMs,
|
|
307
329
|
});
|
|
308
330
|
}
|
|
309
|
-
async relate(subject, relation, object, partition,
|
|
331
|
+
async relate(subject, relation, object, partition, rawOptions) {
|
|
332
|
+
const options = knownWriteOptions(rawOptions);
|
|
310
333
|
this.#assertTransactional(options, 'relate');
|
|
311
334
|
await this.#assertNotFrozen('relate');
|
|
312
335
|
this.#assertDeclared(object, relation);
|
|
@@ -325,7 +348,8 @@ export class RelationsManager {
|
|
|
325
348
|
await this.#write(event, () => this.#driver.relate(subject, relation, object, partition, options));
|
|
326
349
|
await this.#notify(event);
|
|
327
350
|
}
|
|
328
|
-
async unrelate(subject, relation, object, partition,
|
|
351
|
+
async unrelate(subject, relation, object, partition, rawOptions) {
|
|
352
|
+
const options = knownWriteOptions(rawOptions);
|
|
329
353
|
this.#assertTransactional(options, 'unrelate');
|
|
330
354
|
await this.#assertNotFrozen('unrelate');
|
|
331
355
|
this.#assertDeclared(object, relation);
|
|
@@ -403,17 +427,127 @@ export class RelationsManager {
|
|
|
403
427
|
}
|
|
404
428
|
return answers;
|
|
405
429
|
}
|
|
406
|
-
|
|
430
|
+
/**
|
|
431
|
+
* **El deny de relación** (plan 2.5 · lote D · D1; D3/D5/D8/D10). El MISMO embudo que `relate`, en este orden:
|
|
432
|
+
* puerta 1 de `{ transaction }`, barrera del freeze, F-05 (sobre el nombre PÚBLICO), la capacidad
|
|
433
|
+
* `relationDenies` (500 `E_AUTHZ_UNSUPPORTED` con cero llamadas, como `membersOf`: D-28), la puerta del opt-in
|
|
434
|
+
* (solo un tipo en fase `'write'`; sin opt-in, en `'read'` o sobre `group` ⇒ 422 `E_AUTHZ_RELATION_NOT_DENYABLE`,
|
|
435
|
+
* D-17), sin caducidad (D3: `expiresAt` ⇒ 422), id, actor, partición (D4), sujeto (`*` ⇒ 422, userset solo
|
|
436
|
+
* `group#member`), `assertWrite` con `operation: 'deny'`, `#write` (invariante 13) y `#notify`. **Corte de camino**
|
|
437
|
+
* (D5): un deny en `editor` anula entero un grant de `owner` sobre ese objeto; solo sobrevive un `viewer` directo.
|
|
438
|
+
*/
|
|
439
|
+
async deny(subject, relation, object, partition, options) {
|
|
440
|
+
this.#assertTransactional(options, 'deny');
|
|
441
|
+
await this.#assertNotFrozen('deny');
|
|
442
|
+
this.#assertDeclared(object, relation);
|
|
443
|
+
this.#assertDenies('deny');
|
|
444
|
+
assertRelationDenyable(this.#config, object, relation);
|
|
445
|
+
assertDenyNoExpiry(options);
|
|
446
|
+
this.#assertObjectId(object);
|
|
447
|
+
this.#assertActor(options, 'deny');
|
|
448
|
+
assertPartition(partition);
|
|
449
|
+
assertDenySubject(subject);
|
|
450
|
+
this.#assertSubject(subject, partition);
|
|
451
|
+
const ref = { operation: 'deny', subject, relation, object, partition };
|
|
452
|
+
this.#assertWrite(ref, 'deny');
|
|
453
|
+
const event = this.#copyEvent({ ...ref, ...this.#actorOf(options), ...this.#transactional(options) });
|
|
454
|
+
await this.#write(event, () => this.#driver.deny(subject, relation, object, partition, options));
|
|
455
|
+
await this.#notify(event);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* **Retira un deny** (lote D). El embudo de `deny` SIN la puerta del opt-in: acepta CUALQUIER tipo declarado
|
|
459
|
+
* (D8/R2-4 b: retirar el opt-in de un tipo con denies vivos tiene que ser reversible, y los denies se tienen que
|
|
460
|
+
* poder borrar). Ojo, fila declarada (S8–S10 del tester): quitar un deny CONCEDE — `assertWrite` lo ve con
|
|
461
|
+
* `operation: 'removeDeny'`.
|
|
462
|
+
*/
|
|
463
|
+
async removeDeny(subject, relation, object, partition, options) {
|
|
464
|
+
this.#assertTransactional(options, 'removeDeny');
|
|
465
|
+
await this.#assertNotFrozen('removeDeny');
|
|
466
|
+
this.#assertDeclared(object, relation);
|
|
467
|
+
this.#assertDenies('removeDeny');
|
|
468
|
+
assertDenyNoExpiry(options);
|
|
469
|
+
this.#assertObjectId(object);
|
|
470
|
+
this.#assertActor(options, 'removeDeny');
|
|
471
|
+
assertPartition(partition);
|
|
472
|
+
this.#assertSubject(subject, partition);
|
|
473
|
+
const ref = { operation: 'removeDeny', subject, relation, object, partition };
|
|
474
|
+
this.#assertWrite(ref, 'removeDeny');
|
|
475
|
+
const event = this.#copyEvent({ ...ref, ...this.#actorOf(options), ...this.#transactional(options) });
|
|
476
|
+
await this.#write(event, () => this.#driver.removeDeny(subject, relation, object, partition, options));
|
|
477
|
+
await this.#notify(event);
|
|
478
|
+
}
|
|
479
|
+
/** Los sujetos con deny DIRECTO de `relation` sobre `object` (hechos: no resta ni deriva). Cualquier tipo declarado (D8). */
|
|
480
|
+
async listDenies(relation, object, partition) {
|
|
481
|
+
this.#assertDeclared(object, relation);
|
|
482
|
+
this.#assertDenies('listDenies');
|
|
483
|
+
assertPartition(partition);
|
|
484
|
+
this.#assertObjectId(object);
|
|
485
|
+
assertNoListPage('listDenies', arguments[3], this.#driverLabel());
|
|
486
|
+
return this.#driver.listDenies(relation, object, partition);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* **La primitiva de CADENA** (lote D · D9, decisión del dueño 2026-10-14): una decisión por objeto, POR POSICIÓN —
|
|
490
|
+
* `allow` si `check` concede aquí; si no, `deny` si el sujeto tiene un deny vivo (directo o por grupo, anidado)
|
|
491
|
+
* sobre ALGUNA relación de `up*(down*(R))` en el objeto; si no, `none`—. Recorre la cadena de la hoja hacia arriba
|
|
492
|
+
* y para en el primer valor que no sea `none`. Las MISMAS garantías que `checkMany`: tope
|
|
493
|
+
* `RELATIONS_CHECK_MANY_MAX` (50) y `[]` ⇒ 422 `E_AUTHZ_CHECK_MANY_BATCH` con cero llamadas, F-05 y el id POR
|
|
494
|
+
* ÍTEM, un `now` por lote en el driver, orden de salida = orden de entrada, y un error en UN ítem ⇒ 503 para todo
|
|
495
|
+
* el lote (nunca `none` por ítem: `none` en vez de `deny` dejaría que la cadena del llamante siguiera subiendo, S15).
|
|
496
|
+
* La respuesta del driver se valida (una decisión por posición, sin huecos, o 500 `E_AUTHZ_INTERNAL`). Sin
|
|
497
|
+
* `relationDenies` ⇒ 500 `E_AUTHZ_UNSUPPORTED` (D-28): sin deny no hay tri-estado que prometer.
|
|
498
|
+
*/
|
|
499
|
+
async decideMany(subject, relation, objects, partition) {
|
|
500
|
+
assertCheckManyObjects(objects, 'decideMany');
|
|
501
|
+
for (const object of objects) {
|
|
502
|
+
this.#assertDeclared(object, relation);
|
|
503
|
+
this.#assertObjectId(object);
|
|
504
|
+
}
|
|
505
|
+
this.#assertDenies('decideMany');
|
|
506
|
+
assertPartition(partition);
|
|
507
|
+
this.#assertSubject(subject, partition);
|
|
508
|
+
const answers = await this.#driver.decideMany(subject, relation, objects, partition);
|
|
509
|
+
if (!Array.isArray(answers) || answers.length !== objects.length || !everyPositionDecision(answers)) {
|
|
510
|
+
throw new AuthorizationInternalError(`decideMany: el driver de relaciones '${this.#options.driverName ?? 'relations'}' respondió ` +
|
|
511
|
+
`${describeDecisions(answers)} para ${objects.length} objeto(s). ` +
|
|
512
|
+
"La respuesta es un ('allow'|'deny'|'none')[] POR POSICIÓN, exactamente uno por objeto (sin huecos): no se adivina.");
|
|
513
|
+
}
|
|
514
|
+
return answers;
|
|
515
|
+
}
|
|
516
|
+
/** El nombre del driver en los mensajes (la clave en `relations.drivers`, como en las puertas de `{ transaction }`). */
|
|
517
|
+
#driverLabel() {
|
|
518
|
+
return this.#options.driverName ?? 'relations';
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* **D-28 · la capacidad `relationDenies`** (regla D15: opcional, normalizada a `false`): sin ella, o sin el método
|
|
522
|
+
* del puerto, las cuatro operaciones del deny son 500 `E_AUTHZ_UNSUPPORTED` nombrándose, con CERO llamadas al
|
|
523
|
+
* driver — después de F-05 (un tipo no declarado sigue siendo 422), como `membersOf`.
|
|
524
|
+
*/
|
|
525
|
+
#assertDenies(operation) {
|
|
526
|
+
if (this.#driver.capabilities?.relationDenies === true && typeof this.#driver[operation] === 'function')
|
|
527
|
+
return;
|
|
528
|
+
throw new UnsupportedOperationError(operation, 'el deny de relaciones', this.#options.driverName ?? 'relations', `El driver no declara relationDenies: true (o no trae '${operation}'): no sabe escribir ni honrar un deny. ` +
|
|
529
|
+
`Los dos drivers del paquete (database, openfga) lo traen desde 2.5.0-alpha.3.`);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* **Los cuatro listados no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez): `page` salió de la firma
|
|
533
|
+
* de `listObjects`/`listSubjects`/`listDenies`/`membersOf` (nunca hizo nada) y un valor distinto de `undefined` que
|
|
534
|
+
* llegue por JS o `as any` es 500 `E_AUTHZ_UNSUPPORTED` con CERO llamadas al driver (`assertNoListPage`), DESPUÉS de
|
|
535
|
+
* F-05 y de la partición (RC74-5: primero la identidad de la pregunta). Sin página, la lista es completa o lleva
|
|
536
|
+
* `truncated`; nunca un cursor.
|
|
537
|
+
*/
|
|
538
|
+
async listObjects(subject, relation, objectType, partition) {
|
|
407
539
|
this.#assertDeclared({ type: objectType }, relation);
|
|
408
540
|
assertPartition(partition);
|
|
409
541
|
this.#assertSubject(subject, partition);
|
|
410
|
-
|
|
542
|
+
assertNoListPage('listObjects', arguments[4], this.#driverLabel());
|
|
543
|
+
return this.#driver.listObjects(subject, relation, objectType, partition);
|
|
411
544
|
}
|
|
412
|
-
async listSubjects(relation, object, partition
|
|
545
|
+
async listSubjects(relation, object, partition) {
|
|
413
546
|
this.#assertDeclared(object, relation);
|
|
414
547
|
assertPartition(partition);
|
|
415
548
|
this.#assertObjectId(object);
|
|
416
|
-
|
|
549
|
+
assertNoListPage('listSubjects', arguments[3], this.#driverLabel());
|
|
550
|
+
return this.#driver.listSubjects(relation, object, partition);
|
|
417
551
|
}
|
|
418
552
|
/**
|
|
419
553
|
* **Las purgas notifican** (2.4.0-alpha.3 · E, hallazgo #4; D-1/D-3): UN
|
|
@@ -426,7 +560,7 @@ export class RelationsManager {
|
|
|
426
560
|
* notifica también cuando no borra nada: el hecho auditable es «alguien
|
|
427
561
|
* pidió borrar las llaves de X». Sujeta a `requireActor` (D-3, breaking) y
|
|
428
562
|
* al actor validado; NO pasa por `assertWrite` (su `RelationRef.operation`
|
|
429
|
-
* es `'relate' | 'unrelate'`, tipo publicado de R-13). Mismo `#write`
|
|
563
|
+
* es `'relate' | 'unrelate' | 'deny' | 'removeDeny'`, tipo publicado de R-13). Mismo `#write`
|
|
430
564
|
* (invariante 13) y mismo `#notify` que `relate`/`unrelate`.
|
|
431
565
|
*
|
|
432
566
|
* **Y pasan por F-05** (cierre del 🔴 1 del auditor de alpha.3): el mismo
|
|
@@ -475,7 +609,7 @@ export class RelationsManager {
|
|
|
475
609
|
* `E_AUTHZ_UNSUPPORTED` nombrándola (la transitiva sería `ListUsers`, que
|
|
476
610
|
* trunca), jamás un `skip`.
|
|
477
611
|
*/
|
|
478
|
-
async membersOf(object, relation, partition
|
|
612
|
+
async membersOf(object, relation, partition) {
|
|
479
613
|
// Cierre-2 · 🟡 3: `membersOf` toma `(object, relation)` —la forma exacta
|
|
480
614
|
// que F-05 guarda— y era la octava operación fuera de «las siete»; un
|
|
481
615
|
// driver de terceros con `membersOfNative: true` sobre un store compartido
|
|
@@ -488,7 +622,8 @@ export class RelationsManager {
|
|
|
488
622
|
throw new UnsupportedOperationError('membersOf', 'la membresía transitiva de un grupo', 'relations', `Solo el driver 'database' la resuelve (CTE recursiva); en 'openfga' la transitiva sería ` +
|
|
489
623
|
`'ListUsers', que trunca. Usa 'listSubjects' para los hechos directos.`);
|
|
490
624
|
}
|
|
491
|
-
|
|
625
|
+
assertNoListPage('membersOf', arguments[3], this.#driverLabel());
|
|
626
|
+
return this.#driver.membersOf(object, relation, partition);
|
|
492
627
|
}
|
|
493
628
|
/** ORIGEN de `authz:reconcile` de relaciones. 500 `E_AUTHZ_UNSUPPORTED` si el driver no lo trae. */
|
|
494
629
|
async enumerateRelations(partition, page) {
|
|
@@ -496,7 +631,10 @@ export class RelationsManager {
|
|
|
496
631
|
if (!this.#driver.enumerateRelations || !this.#driver.capabilities?.enumerateRelations) {
|
|
497
632
|
throw new UnsupportedOperationError('enumerateRelations', 'ser origen de authz:reconcile de relaciones', 'relations');
|
|
498
633
|
}
|
|
499
|
-
|
|
634
|
+
// 7.4 (injerto 4): la FORMA de la página (limit entero ≥ 1, after cadena no vacía) ⇒ 422 sin tocar el driver; qué
|
|
635
|
+
// cursor es legítimo lo decide el driver (el manager no sabe qué emite).
|
|
636
|
+
const checked = assertEnumeratePage('enumerateRelations', page, 'opaque', this.#driverLabel());
|
|
637
|
+
return this.#driver.enumerateRelations(partition, checked);
|
|
500
638
|
}
|
|
501
639
|
/**
|
|
502
640
|
* **K particiones en UN barrido** (2.4.0-alpha.6) si el driver trae
|
|
@@ -529,9 +667,10 @@ export class RelationsManager {
|
|
|
529
667
|
if (!this.#driver.enumerateRelations || !this.#driver.capabilities?.enumerateRelations) {
|
|
530
668
|
throw new UnsupportedOperationError('enumerateRelationsMany', 'ser origen de authz:reconcile de relaciones', 'relations');
|
|
531
669
|
}
|
|
670
|
+
const checked = assertEnumeratePage('enumerateRelationsMany', page, 'opaque', this.#driverLabel());
|
|
532
671
|
if (typeof this.#driver.enumerateRelationsMany === 'function')
|
|
533
|
-
return this.#driver.enumerateRelationsMany(partitions,
|
|
534
|
-
if (
|
|
672
|
+
return this.#driver.enumerateRelationsMany(partitions, checked);
|
|
673
|
+
if (checked?.after !== undefined) {
|
|
535
674
|
throw new AuthorizationConfigError('enumerateRelationsMany: el driver de relaciones no implementa enumerateRelationsMany y el respaldo del manager (K llamadas ' +
|
|
536
675
|
'a enumerateRelations) no sabe reanudar con un cursor: `after` no se acepta aquí (reanudar aplicaría el cursor de UNA ' +
|
|
537
676
|
'partición a todas y perdería tuplas en silencio). Implementa enumerateRelationsMany en el driver, o pagina partición a ' +
|
|
@@ -539,7 +678,7 @@ export class RelationsManager {
|
|
|
539
678
|
}
|
|
540
679
|
const tuples = [];
|
|
541
680
|
for (const partition of partitions) {
|
|
542
|
-
const one = await this.#driver.enumerateRelations(partition,
|
|
681
|
+
const one = await this.#driver.enumerateRelations(partition, checked);
|
|
543
682
|
if (one.cursor !== undefined) {
|
|
544
683
|
throw new AuthorizationConfigError(`enumerateRelationsMany: la partición ${scopeKey(partition)} tiene más tuplas de las que caben en una página y el driver ` +
|
|
545
684
|
'de relaciones no implementa enumerateRelationsMany: el respaldo del manager (K llamadas a enumerateRelations) no ' +
|
|
@@ -564,4 +703,27 @@ export class RelationsManager {
|
|
|
564
703
|
assertSubject(subject);
|
|
565
704
|
}
|
|
566
705
|
}
|
|
567
|
-
|
|
706
|
+
/**
|
|
707
|
+
* **Las opciones de `relate`/`unrelate` que cruzan al driver** (cierre de R1 · 🟡 2 del auditor + 🟠 C del tester): un
|
|
708
|
+
* objeto NUEVO con SOLO las claves conocidas (`transaction`, `actor`, `expiresAt`, con su presencia: «omitido» ≠
|
|
709
|
+
* `undefined` explícito no importa al driver, pero `'expiresAt' in options` sí al evento), leídas UNA vez — un `Proxy`
|
|
710
|
+
* de la app no llega al driver, ni sus símbolos, ni respuestas distintas entre la validación y la escritura. Lo que no
|
|
711
|
+
* es un objeto pasa tal cual (las validaciones de siempre lo juzgan).
|
|
712
|
+
*/
|
|
713
|
+
function knownWriteOptions(options) {
|
|
714
|
+
if (options === undefined || options === null || typeof options !== 'object')
|
|
715
|
+
return options;
|
|
716
|
+
const out = {};
|
|
717
|
+
for (const key of KNOWN_WRITE_OPTION_KEYS)
|
|
718
|
+
if (key in options)
|
|
719
|
+
out[key] = options[key];
|
|
720
|
+
return out;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* **La lista de claves que cruzan, EXHAUSTIVA por tipo** (cierre-2 de R1 · D3 (b), injerto del analista y del auditor):
|
|
724
|
+
* el `satisfies` exige UNA entrada por cada clave de `RelationWriteOptions` (y ninguna más). Una clave nueva del tipo —una
|
|
725
|
+
* de seguridad, por ejemplo— que no se decida aquí NO COMPILA: hasta el cierre-2 se perdía en silencio en el manager
|
|
726
|
+
* (fallo abierto si la clave era una guarda). Decidir = `true` (cruza al driver) o sacarla del tipo.
|
|
727
|
+
*/
|
|
728
|
+
const KNOWN_WRITE_OPTIONS = { transaction: true, actor: true, expiresAt: true };
|
|
729
|
+
export const KNOWN_WRITE_OPTION_KEYS = Object.freeze(Object.keys(KNOWN_WRITE_OPTIONS));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RelationPage } from '../types.js';
|
|
2
|
+
/** Tamaño de página de `enumerate*` en `database` cuando no se pide `limit`. */
|
|
3
|
+
export declare const RELATIONS_ENUMERATE_DEFAULT_LIMIT = 100;
|
|
4
|
+
/** Techo de `limit` en `enumerate*` de `database`: por encima se RECORTA (con cursor, sin pérdida), no es un error. */
|
|
5
|
+
export declare const RELATIONS_ENUMERATE_MAX_LIMIT = 1000;
|
|
6
|
+
/** Regla 1: un `list*` de relaciones con página (cualquier valor distinto de `undefined`) ⇒ 500 sin tocar el backend. */
|
|
7
|
+
export declare function assertNoListPage(operation: string, page: unknown, driver: string): void;
|
|
8
|
+
/** Qué cursor emite quien valida: `uuid` (database), `none` (openfga, el doble), `opaque` (el manager). */
|
|
9
|
+
export type EnumerateCursorKind = 'uuid' | 'none' | 'opaque';
|
|
10
|
+
/**
|
|
11
|
+
* Regla 2: la forma de la página de `enumerate*`. `undefined` = sin página. Devuelve una COPIA de lo validado (cada campo
|
|
12
|
+
* se lee UNA vez: un getter que cambia entre la validación y el uso no cuela un `NaN` como `limit`, ⚪ 1 del auditor 7.x);
|
|
13
|
+
* quien consulta usa la copia, jamás la página original.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertEnumeratePage(operation: string, page: unknown, cursor: EnumerateCursorKind, driver: string): RelationPage | undefined;
|
|
16
|
+
/** El `limit` efectivo de `enumerate*` (tras `assertEnumeratePage`): el default, o el pedido recortado al techo. */
|
|
17
|
+
export declare function enumerateLimit(page: {
|
|
18
|
+
limit?: number;
|
|
19
|
+
} | undefined): number;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **La página de relaciones, en UN sitio** (lote «antes de RC» · 7.4, opción 4 del juez —`loteRC-74-paginacion-juez.md`,
|
|
3
|
+
* 88,5/100—, decisión del dueño 2026-10-17 (7.4)). Dos reglas, compartidas por el manager, los dos drivers y el doble:
|
|
4
|
+
*
|
|
5
|
+
* 1. **Los `list*` no paginan en 2.5.** `listObjects`/`listSubjects`/`listDenies`/`membersOf` ya no tienen `page` en
|
|
6
|
+
* la firma. `undefined` (ausente o explícito) es «sin página»; CUALQUIER otro valor —`null`, `{}`, `{ after: '' }`,
|
|
7
|
+
* `{ limit: 0 }`…— que llegue por JS o `as any` ⇒ 500 `E_AUTHZ_UNSUPPORTED` nombrando la operación, con CERO
|
|
8
|
+
* llamadas al backend (`assertNoListPage`). Se mira `!== undefined`, jamás `arguments.length` (RC74-3: quien reenvía
|
|
9
|
+
* un opcional vacío no se rompe). Orden de las guardas (RC74-5): F-05 y partición ANTES; la página, la última.
|
|
10
|
+
* 2. **`enumerate*` valida su página** (`assertEnumeratePage`): `limit` entero ≥ 1 o 422 `E_AUTHZ_INVALID_PAGE`
|
|
11
|
+
* (`NaN` generaba una consulta SIN `LIMIT`, tester H2), por encima de `RELATIONS_ENUMERATE_MAX_LIMIT` se RECORTA
|
|
12
|
+
* (`enumerateLimit`: con cursor, sin pérdida; declarado en «Limits»); `after` cadena no vacía o 422, y según el
|
|
13
|
+
* cursor que el driver emite: `uuid` (`database`: el uuid canónico de la fila; un cursor ajeno era un 503 en PG y
|
|
14
|
+
* un fin silencioso en MySQL/SQLite, H3), `none` (`openfga` y el doble no emiten cursor ⇒ 500) u `opaque` (el
|
|
15
|
+
* manager, que no sabe qué emite el driver: solo la forma).
|
|
16
|
+
*
|
|
17
|
+
* Lo que NO hace (H3 a medias, a propósito): atar el cursor a su partición —un cursor de A reutilizado en B salta
|
|
18
|
+
* filas— exige un sobre de cursor; es del lote C / 2.6.
|
|
19
|
+
*/
|
|
20
|
+
import { InvalidRelationPageError, UnsupportedOperationError } from '../errors.js';
|
|
21
|
+
import { isCanonicalUuid } from '../identity.js';
|
|
22
|
+
/** Tamaño de página de `enumerate*` en `database` cuando no se pide `limit`. */
|
|
23
|
+
export const RELATIONS_ENUMERATE_DEFAULT_LIMIT = 100;
|
|
24
|
+
/** Techo de `limit` en `enumerate*` de `database`: por encima se RECORTA (con cursor, sin pérdida), no es un error. */
|
|
25
|
+
export const RELATIONS_ENUMERATE_MAX_LIMIT = 1_000;
|
|
26
|
+
/** Regla 1: un `list*` de relaciones con página (cualquier valor distinto de `undefined`) ⇒ 500 sin tocar el backend. */
|
|
27
|
+
export function assertNoListPage(operation, page, driver) {
|
|
28
|
+
if (page !== undefined)
|
|
29
|
+
throw UnsupportedOperationError.listPage(operation, driver);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Regla 2: la forma de la página de `enumerate*`. `undefined` = sin página. Devuelve una COPIA de lo validado (cada campo
|
|
33
|
+
* se lee UNA vez: un getter que cambia entre la validación y el uso no cuela un `NaN` como `limit`, ⚪ 1 del auditor 7.x);
|
|
34
|
+
* quien consulta usa la copia, jamás la página original.
|
|
35
|
+
*/
|
|
36
|
+
export function assertEnumeratePage(operation, page, cursor, driver) {
|
|
37
|
+
if (page === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (page === null || typeof page !== 'object' || Array.isArray(page)) {
|
|
40
|
+
throw new InvalidRelationPageError(`${operation}: la página tiene que ser un objeto { limit?, after? } (llegó ${describe(page)}).`);
|
|
41
|
+
}
|
|
42
|
+
const { limit, after } = page;
|
|
43
|
+
if (limit !== undefined && !(typeof limit === 'number' && Number.isInteger(limit) && limit >= 1)) {
|
|
44
|
+
throw new InvalidRelationPageError(`${operation}: page.limit tiene que ser un entero ≥ 1 (llegó ${describe(limit)}); por encima de ` +
|
|
45
|
+
`${RELATIONS_ENUMERATE_MAX_LIMIT} se recorta, no es un error.`);
|
|
46
|
+
}
|
|
47
|
+
if (after === undefined)
|
|
48
|
+
return limit === undefined ? {} : { limit };
|
|
49
|
+
if (typeof after !== 'string' || after.length === 0) {
|
|
50
|
+
throw new InvalidRelationPageError(`${operation}: page.after tiene que ser el cursor que devolvió la página anterior (llegó ${describe(after)}).`);
|
|
51
|
+
}
|
|
52
|
+
if (cursor === 'none')
|
|
53
|
+
throw UnsupportedOperationError.enumerateCursor(operation, driver);
|
|
54
|
+
if (cursor === 'uuid' && !isCanonicalUuid(after)) {
|
|
55
|
+
throw new InvalidRelationPageError(`${operation}: page.after no es un cursor de este driver ('${driver}' emite el uuid canónico de la última fila leída); ` +
|
|
56
|
+
`pasa el cursor que devolvió la página anterior, sin tocarlo.`);
|
|
57
|
+
}
|
|
58
|
+
return limit === undefined ? { after } : { limit, after };
|
|
59
|
+
}
|
|
60
|
+
/** El `limit` efectivo de `enumerate*` (tras `assertEnumeratePage`): el default, o el pedido recortado al techo. */
|
|
61
|
+
export function enumerateLimit(page) {
|
|
62
|
+
return Math.min(page?.limit ?? RELATIONS_ENUMERATE_DEFAULT_LIMIT, RELATIONS_ENUMERATE_MAX_LIMIT);
|
|
63
|
+
}
|
|
64
|
+
function describe(value) {
|
|
65
|
+
if (typeof value === 'string')
|
|
66
|
+
return `'${value.slice(0, 40)}'`;
|
|
67
|
+
if (typeof value === 'number')
|
|
68
|
+
return String(value);
|
|
69
|
+
if (value === null)
|
|
70
|
+
return 'null';
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return 'un array';
|
|
73
|
+
return typeof value;
|
|
74
|
+
}
|