@jantstack/adonis-authz 2.5.0-alpha.3 → 2.5.0-rc.1
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 +437 -48
- 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 +17 -3
- package/build/commands/authz_catalog_prune_orphans.js +28 -6
- 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 +0 -1
- package/build/commands/authz_relations_reconcile.js +56 -8
- package/build/commands/authz_scopes_relay.d.ts +0 -1
- package/build/commands/authz_scopes_relay.js +54 -6
- package/build/commands/authz_scopes_requeue.d.ts +0 -1
- package/build/commands/authz_scopes_requeue.js +16 -3
- 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 +10 -1
- package/build/commands/openfga_provision.js +43 -3
- package/build/configure.d.ts +0 -1
- package/build/configure.js +0 -1
- package/build/index.d.ts +9 -7
- package/build/index.js +10 -3
- 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 +27 -2
- 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 +60 -3
- package/build/src/drivers/database_driver.js +160 -25
- package/build/src/drivers/database_relations_driver.d.ts +11 -5
- package/build/src/drivers/database_relations_driver.js +115 -33
- package/build/src/drivers/openfga_driver.d.ts +159 -26
- package/build/src/drivers/openfga_driver.js +882 -199
- package/build/src/drivers/openfga_facts.d.ts +91 -1
- package/build/src/drivers/openfga_facts.js +277 -3
- package/build/src/drivers/openfga_relations_driver.d.ts +41 -13
- package/build/src/drivers/openfga_relations_driver.js +422 -228
- package/build/src/errors.d.ts +160 -1
- package/build/src/errors.js +161 -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 +9 -2
- package/build/src/identity.js +33 -1
- package/build/src/manager.d.ts +24 -1
- package/build/src/manager.js +381 -85
- 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 +2 -3
- package/build/src/openfga.js +4 -2
- 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 +0 -1
- package/build/src/relations/check_many.js +0 -1
- package/build/src/relations/define_relations_config.d.ts +25 -3
- package/build/src/relations/define_relations_config.js +8 -3
- 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 +14 -7
- package/build/src/relations/manager.js +60 -15
- package/build/src/relations/page.d.ts +19 -0
- package/build/src/relations/page.js +74 -0
- package/build/src/relations/reconcile.d.ts +58 -4
- package/build/src/relations/reconcile.js +266 -12
- 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 +5 -4
- package/build/src/relations/tuple_order.js +5 -4
- 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 +36 -3
- package/build/src/scope_outbox.js +78 -18
- package/build/src/shared/backend_guard.d.ts +52 -1
- package/build/src/shared/backend_guard.js +164 -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 +26 -3
- package/build/src/testing/contract.js +258 -20
- package/build/src/testing/main.d.ts +0 -1
- package/build/src/testing/main.js +0 -1
- 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 +7 -1
- package/build/src/testing/relations_contract.js +174 -7
- package/build/src/testing/relations_reconcile_contract.d.ts +0 -1
- package/build/src/testing/relations_reconcile_contract.js +0 -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 +138 -14
- 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/tuple_order.d.ts.map +0 -1
- package/build/src/relations/tuple_order.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
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **El límite de anidación de grupos** (lote «antes de RC» · R1; decisiones del dueño 2026-10-17 (5) «límite SEGÚN EL
|
|
3
|
+
* DRIVER» y (5-bis) opción (a) tras F0 · M-c). Piezas PURAS (sin backend), compartidas por los dos drivers de relaciones
|
|
4
|
+
* y por el plan de `reconcileRelations`, para que la regla sea UNA:
|
|
5
|
+
*
|
|
6
|
+
* - **qué es una arista de anidación**: `group:<Gp>#member@group:<Gc>#member` (los miembros de Gc son miembros de Gp);
|
|
7
|
+
* - **la profundidad es el camino MÁS LARGO** de aristas grupo⊂grupo (T-3 del tester: en un DAG con un camino corto y
|
|
8
|
+
* otro largo hacia la misma cima, OpenFGA da 503 a los NO-miembros por el largo; un BFS por niveles admitiría ese
|
|
9
|
+
* grafo). Un ciclo PREEXISTENTE se cuenta de forma CONSERVADORA: sus k nodos pesan k − 1 saltos (cota superior del
|
|
10
|
+
* camino simple más largo que lo atraviesa) — más negativas, nunca menos;
|
|
11
|
+
* - **la validación de `maxGroupDepth`** (V2-1 del auditor): entero 1..`GROUP_DEPTH_CONFIG_MAX` o `false` (el ÚNICO
|
|
12
|
+
* «apagado»), y nada más — `''`, `NaN`, `0`, `-1`, `Infinity`, `24.5`, `'24'`, `null`, `true` ⇒ 500 al ARRANCAR
|
|
13
|
+
* (`defineRelationsConfig`), jamás un gate apagado en silencio;
|
|
14
|
+
* - **la calibración** (decisión 5 + 5-bis): el techo de `openfga` se MIDE contra el servidor con tuplas contextuales
|
|
15
|
+
* (solo lectura) y es el MÍNIMO de la sonda POSITIVA (un miembro: T) y la NEGATIVA (un no-miembro CON alguna
|
|
16
|
+
* membresía: F; una sonda sin membresías es CIEGA, F a cualquier profundidad, F0 · M-c). La búsqueda es pura sobre
|
|
17
|
+
* una sonda inyectada.
|
|
18
|
+
*
|
|
19
|
+
* Pureza (regla 2): solo depende de los errores. Nada de `manager`/`drivers/*`.
|
|
20
|
+
*/
|
|
21
|
+
import { AuthorizationBackendError, AuthorizationConfigError, GroupDepthLimitError } from '../errors.js';
|
|
22
|
+
import { isRelUserset } from '../types.js';
|
|
23
|
+
/** El tipo y la relación de la anidación (los mismos que `RELATION_USERSET_*`; repetidos para no crear un ciclo de imports). */
|
|
24
|
+
const GROUP = 'group';
|
|
25
|
+
const MEMBER = 'member';
|
|
26
|
+
/**
|
|
27
|
+
* **El techo medido** en OpenFGA v1.19.0 con `--resolve-node-limit` por defecto (25): un miembro resuelve 24 saltos
|
|
28
|
+
* grupo⊂grupo y un no-miembro CON membresías 22 (F0 · M-c, 200/200 en los dos bordes, independiente de `includes` 0..3 y
|
|
29
|
+
* del deny) ⇒ el MÍNIMO, 22. Es la cifra de la fila de «Limits»; la VERDAD de cada despliegue es la calibración.
|
|
30
|
+
*/
|
|
31
|
+
export const RELATIONS_GROUP_DEPTH_MEASURED = 22;
|
|
32
|
+
/**
|
|
33
|
+
* **El mayor `maxGroupDepth` que se acepta** (auditor M4): la calibración usa una cadena de tuplas CONTEXTUALES y el
|
|
34
|
+
* servidor admite 100 por `Check`; la forma de dominio cuesta K + 2. Por encima no hay nada que medir.
|
|
35
|
+
*/
|
|
36
|
+
export const GROUP_DEPTH_CONFIG_MAX = 97;
|
|
37
|
+
/**
|
|
38
|
+
* **El mayor K que la calibración prueba**: la sonda NEGATIVA lleva dos tuplas más (la membresía del no-miembro y la
|
|
39
|
+
* suya propia, M-c) ⇒ K + 4 ≤ 100. Un `maxGroupDepth: 97` es válido en la config y, contra un servidor que resolviera
|
|
40
|
+
* todo, la calibración lo deja en 96 ⇒ 500 (el techo pedido no se puede DEMOSTRAR).
|
|
41
|
+
*/
|
|
42
|
+
export const GROUP_DEPTH_CALIBRATION_MAX = 96;
|
|
43
|
+
/**
|
|
44
|
+
* **Cuántos grupos recorre como mucho la comprobación de UNA arista** (hacia arriba desde Gp más hacia abajo desde Gc).
|
|
45
|
+
* Pasarla ⇒ 422 `E_AUTHZ_GROUP_DEPTH_LIMIT` con motivo `too-many-groups`, nunca «pasa». El recorrido además se corta en
|
|
46
|
+
* cuanto un nivel del BFS supera el techo (el camino más corto ya excede: el largo también).
|
|
47
|
+
*/
|
|
48
|
+
export const GROUP_DEPTH_MAX_NODES = 10_000;
|
|
49
|
+
/**
|
|
50
|
+
* **El presupuesto de la comprobación de UNA arista en `openfga`** (cierre de R1 · 🟠 1 del auditor: amplificación de
|
|
51
|
+
* lecturas contra el store COMPARTIDO). En `openfga` el gate hace un `Read` por grupo de la frontera, y el `Read` hacia
|
|
52
|
+
* ABAJO (objeto + `member`) trae también los HOLDERS —el servidor no admite filtrar por tipo de `user` (`user: 'group:'`
|
|
53
|
+
* ⇒ 400 `validation_error`, medido en :8090 v1.19)—: sin presupuesto, quien controla la anidación hacía pagar O(grupos ×
|
|
54
|
+
* miembros/100) requests a cada `relate`. Pasar cualquiera de los tres ⇒ 422 `E_AUTHZ_GROUP_DEPTH_LIMIT`, motivo
|
|
55
|
+
* `too-many-groups` (fail-closed: no se escribe). Requests (páginas de `Read`), tuplas leídas y un deadline TOTAL del
|
|
56
|
+
* veredicto (comprobado antes de cada request; una request en vuelo la acota el `timeoutMs` del cliente).
|
|
57
|
+
*/
|
|
58
|
+
export const GROUP_DEPTH_MAX_READS = 2_000;
|
|
59
|
+
export const GROUP_DEPTH_MAX_TUPLES = 50_000;
|
|
60
|
+
export const GROUP_DEPTH_DEADLINE_MS = 10_000;
|
|
61
|
+
/**
|
|
62
|
+
* **La espera tras una calibración FALLIDA** (cierre-2 de R1 · D5 (a), injerto del auditor; decisión del dueño 2026-10-20):
|
|
63
|
+
* la calibración de `openfga` son ~14 `Check` (bisección 0..96, dos sondas). Si cae (servidor caído, 503, deadline, el
|
|
64
|
+
* 500 de una sonda ciega), durante esta ventana la siguiente escritura de anidación recibe un error de la MISMA clase —una
|
|
65
|
+
* instancia nueva con el memorizado de `cause`, y un timeout como 503 no-timeout (cierre-3 · R-5)— sin volver a
|
|
66
|
+
* sondear —una flota de `relate` contra un servidor que se cae no lo remata con 14 `Check` cada uno—; pasada, la
|
|
67
|
+
* siguiente vuelve a calibrar (el fallo nunca se memoriza para siempre, C13a). Reloj MONÓTONO del driver.
|
|
68
|
+
*/
|
|
69
|
+
export const GROUP_DEPTH_CALIBRATION_BACKOFF_MS = 2_000;
|
|
70
|
+
/**
|
|
71
|
+
* **La cota de frecuencia de la RE-calibración** (cierre-2 de R1 · D5 (a) + P2 del juez): una lectura de `openfga` que
|
|
72
|
+
* recibe 503 `E_AUTHZ_RESOLUTION_LIMIT` (el servidor ya no resuelve lo que el techo memorizado prometía: el operador BAJÓ
|
|
73
|
+
* `OPENFGA_RESOLVE_NODE_LIMIT` sin reiniciar la app) invalida la calibración memorizada, como mucho UNA vez por este
|
|
74
|
+
* intervalo y por proceso; la siguiente escritura de anidación vuelve a medir. Sin la cota, una forma que dé 503 en cada
|
|
75
|
+
* lectura dispararía una calibración por lectura. SUBIR el límite del servidor no produce ningún 503 que lo delate: eso
|
|
76
|
+
* sigue pidiendo reiniciar la app.
|
|
77
|
+
*/
|
|
78
|
+
export const GROUP_DEPTH_RECALIBRATION_MIN_INTERVAL_MS = 60_000;
|
|
79
|
+
/** Señal INTERNA: el lector la lanza al pasar el presupuesto y `groupNestingVerdict` la convierte en `too-many-groups`. */
|
|
80
|
+
export class GroupDepthBudgetExceeded extends Error {
|
|
81
|
+
budget;
|
|
82
|
+
constructor(budget) {
|
|
83
|
+
super(`presupuesto de la comprobación de anidación agotado (${budget})`);
|
|
84
|
+
this.budget = budget;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** El contador de UN veredicto: `beforeRead()` antes de cada request, `charge(n)` con las tuplas de cada página. */
|
|
88
|
+
export class GroupDepthBudget {
|
|
89
|
+
limits;
|
|
90
|
+
clock;
|
|
91
|
+
reads = 0;
|
|
92
|
+
tuples = 0;
|
|
93
|
+
#start;
|
|
94
|
+
constructor(limits = {
|
|
95
|
+
reads: GROUP_DEPTH_MAX_READS,
|
|
96
|
+
tuples: GROUP_DEPTH_MAX_TUPLES,
|
|
97
|
+
deadlineMs: GROUP_DEPTH_DEADLINE_MS,
|
|
98
|
+
}, clock = () => performance.now()) {
|
|
99
|
+
this.limits = limits;
|
|
100
|
+
this.clock = clock;
|
|
101
|
+
this.#start = clock();
|
|
102
|
+
}
|
|
103
|
+
beforeRead() {
|
|
104
|
+
if (this.clock() - this.#start > this.limits.deadlineMs)
|
|
105
|
+
throw new GroupDepthBudgetExceeded('deadline');
|
|
106
|
+
if (this.reads >= this.limits.reads)
|
|
107
|
+
throw new GroupDepthBudgetExceeded('reads');
|
|
108
|
+
}
|
|
109
|
+
charge(tuples) {
|
|
110
|
+
this.reads += 1;
|
|
111
|
+
this.tuples += tuples;
|
|
112
|
+
if (this.tuples > this.limits.tuples)
|
|
113
|
+
throw new GroupDepthBudgetExceeded('tuples');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* **La marca del PLAN del reconcile** (R1; cierre: 🟡 2 del auditor + 🟠 C del tester): las escrituras de
|
|
118
|
+
* `reconcileRelations` llevan un objeto de opciones creado AQUÍ (`plannedRelateOptions`) porque la profundidad ya la
|
|
119
|
+
* decidió el plan sobre el estado FINAL de la partición (y la negó si excedía). El gate del driver no se vuelve a
|
|
120
|
+
* consultar ahí, por dos razones firmadas: (1) «un ciclo preexistente NO niega» — el gate de escritura rechaza la arista
|
|
121
|
+
* que cierra un ciclo, y negarla a mitad de pasada saltaría una arista que puede transportar un deny (🔴 C-1); (2) un
|
|
122
|
+
* estado INTERMEDIO de la pasada puede exceder sin que exceda el final.
|
|
123
|
+
*
|
|
124
|
+
* **No es falsificable** (hasta el cierre era un símbolo en las opciones y un `Proxy` que responde `true` a todo símbolo
|
|
125
|
+
* apagaba el gate de los DOS drivers; y el gate vive en el driver precisamente para que `manager.driver()` NO se lo
|
|
126
|
+
* salte, así que era la única puerta trasera): la marca es la IDENTIDAD del objeto, registrado en un `WeakMap` privado
|
|
127
|
+
* de este módulo que nada exporta, y está ATADA a la arista (partición, hijo y padre) — un decorador que reciba el objeto
|
|
128
|
+
* real no puede reutilizarlo para OTRA arista —, congelado (no se le cambia la caducidad). Además el `RelationsManager`
|
|
129
|
+
* pasa al driver un objeto NUEVO con solo las claves conocidas, así que por la API nunca llega una marca.
|
|
130
|
+
*/
|
|
131
|
+
const PLANNED = new WeakMap();
|
|
132
|
+
const plannedKey = (partition, child, parent) => JSON.stringify([partition.type, partition.uuid, child, parent]);
|
|
133
|
+
/** Las opciones de `relate` de UNA arista planificada por el reconcile (solo el reconcile del paquete las crea). */
|
|
134
|
+
export function plannedRelateOptions(options, tuple) {
|
|
135
|
+
const frozen = Object.freeze({ ...options });
|
|
136
|
+
if (isGroupNesting(tuple.subject, tuple.relation, tuple.object)) {
|
|
137
|
+
PLANNED.set(frozen, plannedKey(tuple.partition, tuple.subject.object.id, tuple.object.id));
|
|
138
|
+
}
|
|
139
|
+
return frozen;
|
|
140
|
+
}
|
|
141
|
+
/** ¿Llega ESTA arista (`child ⊂ parent` en `partition`) con las opciones que el reconcile creó para ella? */
|
|
142
|
+
export function plannedByReconcile(options, partition, edge) {
|
|
143
|
+
return typeof options === 'object' && options !== null && PLANNED.get(options) === plannedKey(partition, edge.child, edge.parent);
|
|
144
|
+
}
|
|
145
|
+
/** El porqué del techo, para `details.why` (el operador; el `message` es del tenant). */
|
|
146
|
+
const WHY = 'el driver openfga no resuelve de forma fiable una anidación de grupos más profunda: su techo se CALIBRA contra el ' +
|
|
147
|
+
'servidor al primer uso (el mínimo de un miembro y de un no-miembro con membresías; 22 saltos en OpenFGA v1.19 por ' +
|
|
148
|
+
'defecto; se re-calibra tras un 503 de profundidad en lectura, como mucho una vez por minuto) y, pasado, todo check ' +
|
|
149
|
+
'de un no-miembro es 503 E_AUTHZ_RESOLUTION_LIMIT. En database el límite es opt-in (database.enforceGroupDepth) y el ' +
|
|
150
|
+
'número lo declara maxGroupDepth (defineRelationsConfig).';
|
|
151
|
+
const SERVER = 'OPENFGA_RESOLVE_NODE_LIMIT (--resolve-node-limit) en el servidor OpenFGA; maxGroupDepth (defineRelationsConfig) solo puede BAJARLO';
|
|
152
|
+
/** `group:<Gp>#member@group:<Gc>#member`: la ÚNICA forma de anidar (B3: el único userset admitido es `group#member`). */
|
|
153
|
+
export function isGroupNesting(subject, relation, object) {
|
|
154
|
+
return (object?.type === GROUP &&
|
|
155
|
+
relation === MEMBER &&
|
|
156
|
+
isRelUserset(subject) &&
|
|
157
|
+
subject.object?.type === GROUP &&
|
|
158
|
+
subject.relation === MEMBER);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* **Valida `maxGroupDepth`** (V2-1; decisión 5: «entero 1..97 o `false`»): `undefined` ⇒ sin valor (el driver usa su
|
|
162
|
+
* techo), `false` ⇒ apagado EXPLÍCITO, entero en [1, 97] ⇒ ese techo. Cualquier otra cosa ⇒ 500 `E_AUTHZ_CONFIG`: el
|
|
163
|
+
* paquete no lee variables de entorno, y la línea típica `Number(env.get('X') ?? 24)` produce `0` (`''`) y `NaN` (`'abc'`),
|
|
164
|
+
* que con `if (depth > max)` o `if (!max)` APAGARÍAN el gate en silencio.
|
|
165
|
+
*/
|
|
166
|
+
export function assertMaxGroupDepth(value, where = 'defineRelationsConfig') {
|
|
167
|
+
if (value === undefined || value === false)
|
|
168
|
+
return value;
|
|
169
|
+
if (typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= GROUP_DEPTH_CONFIG_MAX)
|
|
170
|
+
return value;
|
|
171
|
+
throw new AuthorizationConfigError(`${where}: maxGroupDepth tiene que ser un entero entre 1 y ${GROUP_DEPTH_CONFIG_MAX} o false (apagado explícito); ` +
|
|
172
|
+
`llegó ${typeof value === 'number' ? String(value) : JSON.stringify(value)} (${typeof value}). Ni 0, ni NaN, ni ` +
|
|
173
|
+
`Infinity, ni una cadena: un valor así apagaría el límite en silencio. El paquete no lee variables de entorno — ` +
|
|
174
|
+
`valida la tuya al arrancar (Env.schema.number.optional() en start/env.ts) y pásala como número. ${GROUP_DEPTH_CONFIG_MAX} ` +
|
|
175
|
+
'es lo máximo que la calibración contra OpenFGA puede medir (100 tuplas contextuales por Check).');
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* **Valida el PAR (`maxGroupDepth`, `enforceGroupDepth`)** — en `defineRelationsConfig` al arrancar y, desde el cierre de
|
|
179
|
+
* R1 (🟡 4 del auditor), OTRA VEZ en los dos drivers: una config extendida con spread
|
|
180
|
+
* (`{ ...defineRelationsConfig(spec), maxGroupDepth: Number(env.get('X')) }`) llegaba al driver sin pasar por la
|
|
181
|
+
* validación, y con `NaN` toda comparación `> limit` es `false`: el gate quedaba APAGADO en silencio. `enforceGroupDepth`
|
|
182
|
+
* solo admite `true`/`false`/ausente y `true` exige un número. Todo lo demás ⇒ 500 `E_AUTHZ_CONFIG`.
|
|
183
|
+
*/
|
|
184
|
+
export function assertGroupDepthSettings(maxGroupDepth, enforceGroupDepth, where = 'defineRelationsConfig') {
|
|
185
|
+
const max = assertMaxGroupDepth(maxGroupDepth, where);
|
|
186
|
+
if (enforceGroupDepth !== undefined && typeof enforceGroupDepth !== 'boolean') {
|
|
187
|
+
throw new AuthorizationConfigError(`${where}: database.enforceGroupDepth tiene que ser true o false (llegó ${JSON.stringify(enforceGroupDepth)}).`);
|
|
188
|
+
}
|
|
189
|
+
if (enforceGroupDepth === true && typeof max !== 'number') {
|
|
190
|
+
throw new AuthorizationConfigError(`${where}: database.enforceGroupDepth: true exige maxGroupDepth NUMÉRICO (llegó ${String(max)}): ` +
|
|
191
|
+
'database no tiene techo técnico que calibrar, así que el número lo pones tú (p. ej. 22, el techo medido de ' +
|
|
192
|
+
'OpenFGA v1.19, si quieres que los datos sean portables a openfga).');
|
|
193
|
+
}
|
|
194
|
+
return { maxGroupDepth: max, enforceGroupDepth: enforceGroupDepth === true };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* **La cadena más larga** (en aristas grupo⊂grupo) de un conjunto de aristas, con los ciclos colapsados de forma
|
|
198
|
+
* CONSERVADORA (cada componente fuertemente conexa de k nodos pesa k − 1). Devuelve la profundidad y una cadena
|
|
199
|
+
* representativa (los grupos, de abajo arriba) para la letra. Lineal en nodos + aristas (Tarjan iterativo + DP).
|
|
200
|
+
*/
|
|
201
|
+
export function groupChainDepth(edges) {
|
|
202
|
+
const up = new Map();
|
|
203
|
+
const nodes = new Set();
|
|
204
|
+
for (const { child, parent } of edges) {
|
|
205
|
+
nodes.add(child);
|
|
206
|
+
nodes.add(parent);
|
|
207
|
+
if (!up.has(child))
|
|
208
|
+
up.set(child, new Set());
|
|
209
|
+
up.get(child).add(parent);
|
|
210
|
+
}
|
|
211
|
+
// Tarjan iterativo (sin recursión: una cadena de 10 000 grupos no puede reventar la pila).
|
|
212
|
+
const index = new Map();
|
|
213
|
+
const low = new Map();
|
|
214
|
+
const onStack = new Set();
|
|
215
|
+
const stack = [];
|
|
216
|
+
const component = new Map();
|
|
217
|
+
const members = [];
|
|
218
|
+
let next = 0;
|
|
219
|
+
for (const root of nodes) {
|
|
220
|
+
if (index.has(root))
|
|
221
|
+
continue;
|
|
222
|
+
const work = [];
|
|
223
|
+
const open = (node) => {
|
|
224
|
+
index.set(node, next);
|
|
225
|
+
low.set(node, next);
|
|
226
|
+
next += 1;
|
|
227
|
+
stack.push(node);
|
|
228
|
+
onStack.add(node);
|
|
229
|
+
work.push({ node, it: (up.get(node) ?? new Set()).values() });
|
|
230
|
+
};
|
|
231
|
+
open(root);
|
|
232
|
+
while (work.length > 0) {
|
|
233
|
+
const frame = work[work.length - 1];
|
|
234
|
+
const step = frame.it.next();
|
|
235
|
+
if (!step.done) {
|
|
236
|
+
const w = step.value;
|
|
237
|
+
if (!index.has(w))
|
|
238
|
+
open(w);
|
|
239
|
+
else if (onStack.has(w))
|
|
240
|
+
low.set(frame.node, Math.min(low.get(frame.node), index.get(w)));
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
work.pop();
|
|
244
|
+
if (work.length > 0) {
|
|
245
|
+
const parentFrame = work[work.length - 1];
|
|
246
|
+
low.set(parentFrame.node, Math.min(low.get(parentFrame.node), low.get(frame.node)));
|
|
247
|
+
}
|
|
248
|
+
if (low.get(frame.node) === index.get(frame.node)) {
|
|
249
|
+
const id = members.length;
|
|
250
|
+
const scc = [];
|
|
251
|
+
let w;
|
|
252
|
+
do {
|
|
253
|
+
w = stack.pop();
|
|
254
|
+
onStack.delete(w);
|
|
255
|
+
component.set(w, id);
|
|
256
|
+
scc.push(w);
|
|
257
|
+
} while (w !== frame.node);
|
|
258
|
+
members.push(scc);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Tarjan emite las componentes en orden topológico INVERSO del grafo hijo→padre: primero las cimas. La DP «camino
|
|
263
|
+
// más largo que EMPIEZA en c» recorre en ese orden (los padres de c ya están resueltos).
|
|
264
|
+
const best = new Array(members.length).fill(0);
|
|
265
|
+
const nextOf = new Array(members.length).fill(-1);
|
|
266
|
+
for (let c = 0; c < members.length; c++) {
|
|
267
|
+
const own = members[c].length - 1;
|
|
268
|
+
let tail = 0;
|
|
269
|
+
let via = -1;
|
|
270
|
+
for (const node of members[c]) {
|
|
271
|
+
for (const parent of up.get(node) ?? []) {
|
|
272
|
+
const pc = component.get(parent);
|
|
273
|
+
if (pc === c)
|
|
274
|
+
continue;
|
|
275
|
+
if (1 + best[pc] > tail) {
|
|
276
|
+
tail = 1 + best[pc];
|
|
277
|
+
via = pc;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
best[c] = own + tail;
|
|
282
|
+
nextOf[c] = via;
|
|
283
|
+
}
|
|
284
|
+
let start = -1;
|
|
285
|
+
for (let c = 0; c < members.length; c++)
|
|
286
|
+
if (start < 0 || best[c] > best[start])
|
|
287
|
+
start = c;
|
|
288
|
+
const chain = [];
|
|
289
|
+
for (let c = start; c >= 0; c = nextOf[c])
|
|
290
|
+
chain.push(...[...members[c]].sort());
|
|
291
|
+
return { depth: start < 0 ? 0 : best[start], chain };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* **El veredicto de la arista `child ⊂ parent`** (gate de escritura de R1): recorre hacia ARRIBA desde `parent` y hacia
|
|
295
|
+
* ABAJO desde `child` (BFS por niveles, una lectura por nivel), y decide:
|
|
296
|
+
*
|
|
297
|
+
* - `cycle` si `child` es `parent` o aparece por encima de `parent` (la arista lo cerraría);
|
|
298
|
+
* - `too-deep` si el camino MÁS LARGO que atraviesa la arista (`alto(parent) + 1 + bajo(child)`) supera `limit` — y
|
|
299
|
+
* también, sin terminar el recorrido, en cuanto los niveles del BFS ya lo superan (el camino corto es una cota
|
|
300
|
+
* inferior del largo);
|
|
301
|
+
* - `too-many-groups` si la región pasa de `maxNodes`, o si el LECTOR agota su presupuesto (`GroupDepthBudgetExceeded`:
|
|
302
|
+
* requests, tuplas o deadline — `openfga`, cierre de R1 · 🟠 1 del auditor);
|
|
303
|
+
* - `ok` en otro caso.
|
|
304
|
+
*
|
|
305
|
+
* La arista misma (si ya existe: renovación) no se cuenta dos veces.
|
|
306
|
+
*/
|
|
307
|
+
export async function groupNestingVerdict(edge, limit, reader, maxNodes = GROUP_DEPTH_MAX_NODES) {
|
|
308
|
+
try {
|
|
309
|
+
return await walkGroupNesting(edge, limit, reader, maxNodes);
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (error instanceof GroupDepthBudgetExceeded)
|
|
313
|
+
return { kind: 'too-many-groups', depth: null, budget: error.budget };
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function walkGroupNesting(edge, limit, reader, maxNodes) {
|
|
318
|
+
if (edge.child === edge.parent)
|
|
319
|
+
return { kind: 'cycle', depth: null };
|
|
320
|
+
const region = [edge];
|
|
321
|
+
const seen = new Set([edge.child, edge.parent]);
|
|
322
|
+
const isSelf = (e) => e.child === edge.child && e.parent === edge.parent;
|
|
323
|
+
// Hacia ARRIBA desde `parent`.
|
|
324
|
+
let frontier = [edge.parent];
|
|
325
|
+
let upLevels = 0;
|
|
326
|
+
const above = new Set([edge.parent]);
|
|
327
|
+
while (frontier.length > 0) {
|
|
328
|
+
const found = (await reader.parentsOf(frontier)).filter((e) => !isSelf(e));
|
|
329
|
+
const nextFrontier = [];
|
|
330
|
+
for (const e of found) {
|
|
331
|
+
region.push(e);
|
|
332
|
+
if (e.parent === edge.child)
|
|
333
|
+
return { kind: 'cycle', depth: null };
|
|
334
|
+
if (!above.has(e.parent)) {
|
|
335
|
+
above.add(e.parent);
|
|
336
|
+
nextFrontier.push(e.parent);
|
|
337
|
+
}
|
|
338
|
+
seen.add(e.parent);
|
|
339
|
+
}
|
|
340
|
+
if (nextFrontier.length > 0)
|
|
341
|
+
upLevels += 1;
|
|
342
|
+
if (upLevels + 1 > limit)
|
|
343
|
+
return { kind: 'too-deep', depth: upLevels + 1, chain: [] };
|
|
344
|
+
if (seen.size > maxNodes)
|
|
345
|
+
return { kind: 'too-many-groups', depth: null, budget: 'nodes' };
|
|
346
|
+
frontier = nextFrontier;
|
|
347
|
+
}
|
|
348
|
+
// Hacia ABAJO desde `child`.
|
|
349
|
+
frontier = [edge.child];
|
|
350
|
+
let downLevels = 0;
|
|
351
|
+
const below = new Set([edge.child]);
|
|
352
|
+
while (frontier.length > 0) {
|
|
353
|
+
const found = (await reader.childrenOf(frontier)).filter((e) => !isSelf(e));
|
|
354
|
+
const nextFrontier = [];
|
|
355
|
+
for (const e of found) {
|
|
356
|
+
region.push(e);
|
|
357
|
+
if (e.child === edge.parent || above.has(e.child))
|
|
358
|
+
return { kind: 'cycle', depth: null };
|
|
359
|
+
if (!below.has(e.child)) {
|
|
360
|
+
below.add(e.child);
|
|
361
|
+
nextFrontier.push(e.child);
|
|
362
|
+
}
|
|
363
|
+
seen.add(e.child);
|
|
364
|
+
}
|
|
365
|
+
if (nextFrontier.length > 0)
|
|
366
|
+
downLevels += 1;
|
|
367
|
+
if (upLevels + 1 + downLevels > limit)
|
|
368
|
+
return { kind: 'too-deep', depth: upLevels + 1 + downLevels, chain: [] };
|
|
369
|
+
if (seen.size > maxNodes)
|
|
370
|
+
return { kind: 'too-many-groups', depth: null, budget: 'nodes' };
|
|
371
|
+
frontier = nextFrontier;
|
|
372
|
+
}
|
|
373
|
+
const { depth, chain } = groupChainDepth(region);
|
|
374
|
+
if (depth > limit)
|
|
375
|
+
return { kind: 'too-deep', depth, chain };
|
|
376
|
+
return { kind: 'ok', depth };
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* **El error de un veredicto que no es `ok`** (cierre-3 de R1 · R-2 del re-ataque; decisión firmada (3): lo transitorio es
|
|
380
|
+
* 503): el agotamiento del DEADLINE del presupuesto (`budget: 'deadline'`) no dice nada de los datos —el mismo grafo da
|
|
381
|
+
* `ok` con el servidor descargado— sino que el backend va LENTO (o lo satura otro tenant del store compartido), así que es
|
|
382
|
+
* 503 `AuthorizationBackendError` transitorio —NO la subclase timeout: la escritura no ocurrió y el manager no notifica
|
|
383
|
+
* `indeterminate` (invariante 13)— y nunca el 422 «aplana la anidación», que un cliente no reintenta. Las otras cotas
|
|
384
|
+
* (`nodes`, `reads`, `tuples`) son deterministas sobre los datos y siguen siendo 422 `too-many-groups`.
|
|
385
|
+
*/
|
|
386
|
+
export function groupNestingError(driver, verdict, edge, limit) {
|
|
387
|
+
if (verdict.kind === 'too-many-groups' && verdict.budget === 'deadline') {
|
|
388
|
+
const error = new AuthorizationBackendError(driver, 'relate', new Error(`la comprobación de anidación de grupos agotó su deadline total de ${GROUP_DEPTH_DEADLINE_MS} ms`));
|
|
389
|
+
error.message =
|
|
390
|
+
`El backend de autorización '${driver}' no respondió a tiempo para comprobar la arista de anidación ${edge} ` +
|
|
391
|
+
`(deadline total del veredicto: ${GROUP_DEPTH_DEADLINE_MS} ms; relate). Es transitorio (el servidor va lento): ` +
|
|
392
|
+
'reintenta. No se ha escrito nada.';
|
|
393
|
+
return error;
|
|
394
|
+
}
|
|
395
|
+
return groupDepthLimitError(verdict, edge, limit);
|
|
396
|
+
}
|
|
397
|
+
/** El 422 de un veredicto que no es `ok` (letra de `E_AUTHZ_SCOPE_DEPTH_LIMIT`: el `message` no nombra el backend). */
|
|
398
|
+
export function groupDepthLimitError(verdict, edge, limit) {
|
|
399
|
+
const head = verdict.kind === 'cycle'
|
|
400
|
+
? `relate: la arista de anidación ${edge} cerraría un CICLO de grupos (el grupo contenedor ya es miembro, directa o ` +
|
|
401
|
+
'transitivamente, del que quieres meter)'
|
|
402
|
+
: verdict.kind === 'too-many-groups'
|
|
403
|
+
? `relate: la arista de anidación ${edge} no se puede comprobar: la región de grupos anidados que habría que recorrer ` +
|
|
404
|
+
(verdict.budget === 'nodes'
|
|
405
|
+
? `pasa de ${GROUP_DEPTH_MAX_NODES} grupos`
|
|
406
|
+
: `agota el presupuesto de la comprobación (${verdict.budget === 'reads' ? `${GROUP_DEPTH_MAX_READS} lecturas` : verdict.budget === 'tuples' ? `${GROUP_DEPTH_MAX_TUPLES} tuplas leídas` : `${GROUP_DEPTH_DEADLINE_MS} ms`})`)
|
|
407
|
+
: `relate: la arista de anidación ${edge} dejaría una cadena de ${verdict.chain.length === 0 ? 'al menos ' : ''}` +
|
|
408
|
+
`${verdict.depth} saltos de grupos anidados (grupo⊂grupo, el camino MÁS LARGO que la atraviesa) y el techo es ` +
|
|
409
|
+
`${limit} (maxGroupDepth de la config de relaciones, o el techo del driver)`;
|
|
410
|
+
return new GroupDepthLimitError(`${head}. No se ha escrito nada. Aplana la anidación (mete a los miembros en un grupo menos profundo) o, si el techo ` +
|
|
411
|
+
'lo fija tu config, revísalo con quien opera el servicio.', {
|
|
412
|
+
reason: verdict.kind,
|
|
413
|
+
edge,
|
|
414
|
+
depth: verdict.depth,
|
|
415
|
+
maxGroupDepth: limit,
|
|
416
|
+
...(verdict.kind === 'too-many-groups' ? { budget: verdict.budget } : {}),
|
|
417
|
+
why: verdict.kind === 'cycle' ? `un ciclo no tiene profundidad definida y el paquete no lo admite al escribir. ${WHY}` : WHY,
|
|
418
|
+
server: SERVER,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* **La calibración del techo** (decisión 5 + 5-bis, opción (a)): el mayor K en [0, `max`] para el que la sonda
|
|
423
|
+
* POSITIVA (un miembro, T) Y la NEGATIVA (un no-miembro con membresía, F) resuelven — el MÍNIMO de las dos —, por
|
|
424
|
+
* bisección (el borde es determinista en relaciones, M-c: 200/200). Una sonda que da la OTRA respuesta (`blind`: el
|
|
425
|
+
* miembro F, el no-miembro T) es un servidor o un modelo que no evalúa como el medido ⇒ 500, jamás «pasa». Cualquier
|
|
426
|
+
* otro fallo de la sonda (503, deadline) se PROPAGA: quien llama no lo memoriza.
|
|
427
|
+
*/
|
|
428
|
+
export async function calibrateGroupDepth(probe, max = GROUP_DEPTH_CALIBRATION_MAX) {
|
|
429
|
+
const passes = async (k) => {
|
|
430
|
+
for (const side of ['positive', 'negative']) {
|
|
431
|
+
const outcome = await probe(k, side);
|
|
432
|
+
if (outcome === 'blind') {
|
|
433
|
+
throw new AuthorizationConfigError(`calibración del techo de anidación de grupos: la sonda ${side === 'positive' ? 'POSITIVA (un miembro)' : 'NEGATIVA (un no-miembro con membresía)'} ` +
|
|
434
|
+
`con ${k} saltos dio la respuesta CONTRARIA a la esperada. El servidor no evalúa la anidación como el paquete ` +
|
|
435
|
+
'midió (OpenFGA v1.19): no se puede fijar un techo con él, y el paquete no escribe a ciegas. Revisa la versión ' +
|
|
436
|
+
'del servidor y el modelo fijado.');
|
|
437
|
+
}
|
|
438
|
+
if (outcome === 'too-complex')
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
};
|
|
443
|
+
let lo = 0;
|
|
444
|
+
let hi = max + 1;
|
|
445
|
+
while (hi - lo > 1) {
|
|
446
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
447
|
+
if (await passes(mid))
|
|
448
|
+
lo = mid;
|
|
449
|
+
else
|
|
450
|
+
hi = mid;
|
|
451
|
+
}
|
|
452
|
+
return lo;
|
|
453
|
+
}
|
|
@@ -64,8 +64,8 @@ export declare class RelationsManager {
|
|
|
64
64
|
* lo que lee la puerta 1.
|
|
65
65
|
*/
|
|
66
66
|
capabilities(): Readonly<RelationsDriverCapabilities>;
|
|
67
|
-
relate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef,
|
|
68
|
-
unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef,
|
|
67
|
+
relate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, rawOptions?: RelationWriteOptions): Promise<void>;
|
|
68
|
+
unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, rawOptions?: RelationWriteOptions): Promise<void>;
|
|
69
69
|
/**
|
|
70
70
|
* **Las lecturas también pasan por F-05** (2.4.0-alpha.3, 🟠 2 del auditor):
|
|
71
71
|
* el `objectId` lleva la partición, así que `listSubjects('assignee',
|
|
@@ -115,7 +115,7 @@ export declare class RelationsManager {
|
|
|
115
115
|
*/
|
|
116
116
|
removeDeny(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
|
|
117
117
|
/** Los sujetos con deny DIRECTO de `relation` sobre `object` (hechos: no resta ni deriva). Cualquier tipo declarado (D8). */
|
|
118
|
-
listDenies(relation: string, object: RelObject, partition: ScopeRef
|
|
118
|
+
listDenies(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
119
119
|
/**
|
|
120
120
|
* **La primitiva de CADENA** (lote D · D9, decisión del dueño 2026-10-14): una decisión por objeto, POR POSICIÓN —
|
|
121
121
|
* `allow` si `check` concede aquí; si no, `deny` si el sujeto tiene un deny vivo (directo o por grupo, anidado)
|
|
@@ -128,8 +128,15 @@ export declare class RelationsManager {
|
|
|
128
128
|
* `relationDenies` ⇒ 500 `E_AUTHZ_UNSUPPORTED` (D-28): sin deny no hay tri-estado que prometer.
|
|
129
129
|
*/
|
|
130
130
|
decideMany(subject: RelSubject, relation: string, objects: RelObject[], partition: ScopeRef): Promise<RelationDecision[]>;
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
/**
|
|
132
|
+
* **Los cuatro listados no paginan en 2.5** (lote «antes de RC» · 7.4, opción 4 del juez): `page` salió de la firma
|
|
133
|
+
* de `listObjects`/`listSubjects`/`listDenies`/`membersOf` (nunca hizo nada) y un valor distinto de `undefined` que
|
|
134
|
+
* llegue por JS o `as any` es 500 `E_AUTHZ_UNSUPPORTED` con CERO llamadas al driver (`assertNoListPage`), DESPUÉS de
|
|
135
|
+
* F-05 y de la partición (RC74-5: primero la identidad de la pregunta). Sin página, la lista es completa o lleva
|
|
136
|
+
* `truncated`; nunca un cursor.
|
|
137
|
+
*/
|
|
138
|
+
listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef): Promise<RelationObjectsPage>;
|
|
139
|
+
listSubjects(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
133
140
|
/**
|
|
134
141
|
* **Las purgas notifican** (2.4.0-alpha.3 · E, hallazgo #4; D-1/D-3): UN
|
|
135
142
|
* evento por llamada —aunque el driver barra las dos ortografías del uuid
|
|
@@ -159,7 +166,7 @@ export declare class RelationsManager {
|
|
|
159
166
|
* `E_AUTHZ_UNSUPPORTED` nombrándola (la transitiva sería `ListUsers`, que
|
|
160
167
|
* trunca), jamás un `skip`.
|
|
161
168
|
*/
|
|
162
|
-
membersOf(object: RelObject, relation: string, partition: ScopeRef
|
|
169
|
+
membersOf(object: RelObject, relation: string, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
163
170
|
/** ORIGEN de `authz:reconcile` de relaciones. 500 `E_AUTHZ_UNSUPPORTED` si el driver no lo trae. */
|
|
164
171
|
enumerateRelations(partition: ScopeRef, page?: RelationPage): Promise<RelationTuplePage>;
|
|
165
172
|
/**
|
|
@@ -186,4 +193,4 @@ export declare class RelationsManager {
|
|
|
186
193
|
*/
|
|
187
194
|
enumerateRelationsMany(partitions: ScopeRef[], page?: RelationPage): Promise<RelationTuplePage>;
|
|
188
195
|
}
|
|
189
|
-
|
|
196
|
+
export declare const KNOWN_WRITE_OPTION_KEYS: readonly (keyof RelationWriteOptions)[];
|