@jantstack/adonis-authz 2.0.0-alpha.1 → 2.4.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +693 -36
- package/build/commands/authz_catalog_diff.js +1 -1
- package/build/commands/authz_catalog_diff.js.map +1 -1
- package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
- package/build/commands/authz_catalog_prune_orphans.js +136 -0
- package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
- package/build/commands/authz_catalog_sync.d.ts +17 -0
- package/build/commands/authz_catalog_sync.d.ts.map +1 -1
- package/build/commands/authz_catalog_sync.js +27 -4
- package/build/commands/authz_catalog_sync.js.map +1 -1
- package/build/commands/authz_freeze.d.ts +44 -0
- package/build/commands/authz_freeze.d.ts.map +1 -0
- package/build/commands/authz_freeze.js +95 -0
- package/build/commands/authz_freeze.js.map +1 -0
- package/build/commands/authz_reconcile.d.ts +102 -0
- package/build/commands/authz_reconcile.d.ts.map +1 -0
- package/build/commands/authz_reconcile.js +294 -0
- package/build/commands/authz_reconcile.js.map +1 -0
- package/build/commands/authz_relations_reconcile.d.ts +97 -0
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
- package/build/commands/authz_relations_reconcile.js +313 -0
- package/build/commands/authz_relations_reconcile.js.map +1 -0
- package/build/commands/authz_scopes_relay.d.ts +47 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -0
- package/build/commands/authz_scopes_relay.js +141 -0
- package/build/commands/authz_scopes_relay.js.map +1 -0
- package/build/commands/authz_unfreeze.d.ts +37 -0
- package/build/commands/authz_unfreeze.d.ts.map +1 -0
- package/build/commands/authz_unfreeze.js +92 -0
- package/build/commands/authz_unfreeze.js.map +1 -0
- package/build/commands/main.d.ts +6 -1
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +6 -1
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +46 -4
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +90 -7
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/configure.d.ts +11 -0
- package/build/configure.d.ts.map +1 -1
- package/build/configure.js +37 -1
- package/build/configure.js.map +1 -1
- package/build/index.d.ts +50 -10
- package/build/index.d.ts.map +1 -1
- package/build/index.js +45 -6
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +26 -2
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +55 -2
- package/build/providers/authz_provider.js.map +1 -1
- package/build/services/relations.d.ts +14 -0
- package/build/services/relations.d.ts.map +1 -0
- package/build/services/relations.js +17 -0
- package/build/services/relations.js.map +1 -0
- package/build/src/{catalog.d.ts → catalog/catalog.d.ts} +41 -2
- package/build/src/catalog/catalog.d.ts.map +1 -0
- package/build/src/{catalog.js → catalog/catalog.js} +120 -14
- package/build/src/catalog/catalog.js.map +1 -0
- package/build/src/{catalog_cache.d.ts → catalog/catalog_cache.d.ts} +46 -22
- package/build/src/catalog/catalog_cache.d.ts.map +1 -0
- package/build/src/{catalog_cache.js → catalog/catalog_cache.js} +53 -43
- package/build/src/catalog/catalog_cache.js.map +1 -0
- package/build/src/define_config.d.ts +137 -3
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts +128 -7
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +510 -24
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +113 -0
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/database_relations_driver.js +679 -0
- package/build/src/drivers/database_relations_driver.js.map +1 -0
- package/build/src/drivers/openfga_driver.d.ts +729 -122
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +2083 -476
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +384 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -0
- package/build/src/drivers/openfga_facts.js +836 -0
- package/build/src/drivers/openfga_facts.js.map +1 -0
- package/build/src/drivers/openfga_relations_driver.d.ts +141 -0
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/openfga_relations_driver.js +590 -0
- package/build/src/drivers/openfga_relations_driver.js.map +1 -0
- package/build/src/errors.d.ts +290 -5
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +296 -7
- package/build/src/errors.js.map +1 -1
- package/build/src/freeze.d.ts +141 -0
- package/build/src/freeze.d.ts.map +1 -0
- package/build/src/freeze.js +217 -0
- package/build/src/freeze.js.map +1 -0
- package/build/src/http/app_access_middleware.d.ts.map +1 -0
- package/build/src/http/app_access_middleware.js.map +1 -0
- package/build/src/http/resource_access_middleware.d.ts +105 -0
- package/build/src/http/resource_access_middleware.d.ts.map +1 -0
- package/build/src/http/resource_access_middleware.js +81 -0
- package/build/src/http/resource_access_middleware.js.map +1 -0
- package/build/src/identity.d.ts +74 -1
- package/build/src/identity.d.ts.map +1 -1
- package/build/src/identity.js +100 -2
- package/build/src/identity.js.map +1 -1
- package/build/src/manager.d.ts +330 -4
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +1285 -188
- package/build/src/manager.js.map +1 -1
- package/build/src/models/authz_assignment.d.ts +7 -7
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +7 -7
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +7 -7
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_role.d.ts +7 -7
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +7 -7
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/openfga.d.ts +18 -2
- package/build/src/openfga.d.ts.map +1 -1
- package/build/src/openfga.js +15 -1
- package/build/src/openfga.js.map +1 -1
- package/build/src/reconcile.d.ts +37 -0
- package/build/src/reconcile.d.ts.map +1 -0
- package/build/src/reconcile.js +69 -0
- package/build/src/reconcile.js.map +1 -0
- package/build/src/relation_partition_trigger.d.ts +8 -0
- package/build/src/relation_partition_trigger.d.ts.map +1 -0
- package/build/src/relation_partition_trigger.js +93 -0
- package/build/src/relation_partition_trigger.js.map +1 -0
- package/build/src/relations/define_relations_config.d.ts +75 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -0
- package/build/src/relations/define_relations_config.js +173 -0
- package/build/src/relations/define_relations_config.js.map +1 -0
- package/build/src/relations/manager.d.ts +60 -0
- package/build/src/relations/manager.d.ts.map +1 -0
- package/build/src/relations/manager.js +213 -0
- package/build/src/relations/manager.js.map +1 -0
- package/build/src/relations/reconcile.d.ts +111 -0
- package/build/src/relations/reconcile.d.ts.map +1 -0
- package/build/src/relations/reconcile.js +200 -0
- package/build/src/relations/reconcile.js.map +1 -0
- package/build/src/relations_config_store.d.ts +22 -0
- package/build/src/relations_config_store.d.ts.map +1 -0
- package/build/src/relations_config_store.js +74 -0
- package/build/src/relations_config_store.js.map +1 -0
- package/build/src/scope_outbox.d.ts +69 -0
- package/build/src/scope_outbox.d.ts.map +1 -0
- package/build/src/scope_outbox.js +298 -0
- package/build/src/scope_outbox.js.map +1 -0
- package/build/src/{drivers → shared}/backend_guard.d.ts +42 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -0
- package/build/src/{drivers → shared}/backend_guard.js +78 -1
- package/build/src/shared/backend_guard.js.map +1 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -0
- package/build/src/shared/sql_expiry.js.map +1 -0
- package/build/src/shared/transaction_guard.d.ts +50 -0
- package/build/src/shared/transaction_guard.d.ts.map +1 -0
- package/build/src/shared/transaction_guard.js +60 -0
- package/build/src/shared/transaction_guard.js.map +1 -0
- package/build/src/sql_descendants.d.ts +47 -1
- package/build/src/sql_descendants.d.ts.map +1 -1
- package/build/src/sql_descendants.js +75 -1
- package/build/src/sql_descendants.js.map +1 -1
- package/build/src/testing/contract.d.ts +129 -4
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +912 -177
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +8 -2
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +4 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +284 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -0
- package/build/src/testing/migration_contract.js +586 -0
- package/build/src/testing/migration_contract.js.map +1 -0
- package/build/src/testing/relations_contract.d.ts +73 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -0
- package/build/src/testing/relations_contract.js +1069 -0
- package/build/src/testing/relations_contract.js.map +1 -0
- package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
- package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
- package/build/src/testing/relations_reconcile_contract.js +220 -0
- package/build/src/testing/relations_reconcile_contract.js.map +1 -0
- package/build/src/traits/authz_scopes.js +1 -1
- package/build/src/traits/authz_scopes.js.map +1 -1
- package/build/src/traits/has_uuid.d.ts +8 -8
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +1053 -83
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +10 -0
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/authorization.stub +104 -4
- package/build/stubs/migration.stub +136 -0
- package/build/stubs/scopes_outbox_migration.stub +57 -0
- package/package.json +4 -2
- package/build/commands/openfga_import.d.ts +0 -34
- package/build/commands/openfga_import.d.ts.map +0 -1
- package/build/commands/openfga_import.js +0 -97
- package/build/commands/openfga_import.js.map +0 -1
- package/build/src/catalog.d.ts.map +0 -1
- package/build/src/catalog.js.map +0 -1
- package/build/src/catalog_cache.d.ts.map +0 -1
- package/build/src/catalog_cache.js.map +0 -1
- package/build/src/drivers/backend_guard.d.ts.map +0 -1
- package/build/src/drivers/backend_guard.js.map +0 -1
- package/build/src/drivers/sql_expiry.d.ts.map +0 -1
- package/build/src/drivers/sql_expiry.js.map +0 -1
- package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
- package/build/src/middleware/app_access_middleware.js.map +0 -1
- /package/build/src/{middleware → http}/app_access_middleware.d.ts +0 -0
- /package/build/src/{middleware → http}/app_access_middleware.js +0 -0
- /package/build/src/{drivers → shared}/sql_expiry.d.ts +0 -0
- /package/build/src/{drivers → shared}/sql_expiry.js +0 -0
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
import type { ClientBatchCheckItem, ClientBatchCheckSingleResponse } from '@openfga/sdk';
|
|
2
|
-
import type { AuthorizationDriver, DenyRef, GrantOptions, GrantOutcome, HolderTypeMap, RoleQuery, ScopeChainResolver, ScopeRef, ScopeType, SubjectRef } from '../types.js';
|
|
3
|
-
import { CatalogCache } from '../catalog_cache.js';
|
|
4
|
-
import type { CatalogRevalidate, CatalogRoleRef } from '../catalog_cache.js';
|
|
2
|
+
import type { AuthorizationDriver, CatalogProjection, DenyRef, GrantOptions, GrantOutcome, HolderTypeMap, ReconcileFactPage, ReconcileOptions, ReconcileReport, ReconcileSource, RoleQuery, ScopeChainResolver, ScopeOutbox, ScopeRef, ScopeType, SubjectRef, WriteOptions } from '../types.js';
|
|
3
|
+
import { CatalogCache } from '../catalog/catalog_cache.js';
|
|
4
|
+
import type { CatalogRevalidate, CatalogRoleRef } from '../catalog/catalog_cache.js';
|
|
5
|
+
import { assertHolderTypes } from './openfga_facts.js';
|
|
6
|
+
import type { FactsRelationsConfig } from './openfga_facts.js';
|
|
5
7
|
import type { Clock } from '../clock.js';
|
|
6
8
|
/**
|
|
7
|
-
* Driver `openfga` —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Driver `openfga` — **modo `facts` y solo `facts`** (3b-2k · K2). Los
|
|
10
|
+
* HECHOS (asignaciones y denies), el ÁRBOL de scopes y la PROYECCIÓN del
|
|
11
|
+
* catálogo viven en un servidor OpenFGA, que es el PDP: `authorize` es UN
|
|
12
|
+
* solo `Check` y no consulta el árbol del consumidor. El CATÁLOGO sigue
|
|
13
|
+
* siendo propiedad local en las tablas `authz_*` (la proyección es derivada,
|
|
14
|
+
* reconstruible y nunca se lee como catálogo).
|
|
11
15
|
*
|
|
12
|
-
* Modelo
|
|
13
|
-
* - `
|
|
14
|
-
* - `
|
|
15
|
-
* (3A · A1: el id lleva el UUID del catálogo, nunca el slug
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Modelo (c2r), en `openfga_facts.ts` (`openFgaFactsModel`):
|
|
17
|
+
* - `role:<roleUuid>#permits_<P>@<holder>:*` → la proyección del catálogo
|
|
18
|
+
* - `role_binding:<scopeKey>|<roleUuid>` #assignee → la asignación
|
|
19
|
+
* (3A · A1: el id lleva el UUID del catálogo, nunca el slug; se parsea
|
|
20
|
+
* desde la derecha), `#role` → su rol
|
|
21
|
+
* - `scope:<scopeKey>` con `#parent` (el árbol), `#binding` (dónde es
|
|
22
|
+
* visible el rol), `#denied_<P>` (el deny explícito) y `#rooted` (la
|
|
23
|
+
* alcanzabilidad de la raíz: `can_<P> = (<P> but not denied_<P>) and
|
|
24
|
+
* rooted`, 3b-2i)
|
|
19
25
|
* - Expiración vía condition `not_expired` (valid_until en la tupla,
|
|
20
26
|
* current_time en cada check) — ni scheduler necesita.
|
|
21
27
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
28
|
+
* **El modo `resolver` ya no existe** (3b-2k · K2, breaking): hasta 2.2 este
|
|
29
|
+
* driver expandía la cadena del consumidor a un `batchCheck` de N×M y
|
|
30
|
+
* guardaba los denies en objetos `deny_binding` propios. Con (c2r) esa rama
|
|
31
|
+
* era código muerto: se borró entera, y con ella `openFgaAuthorizationModel`
|
|
32
|
+
* y `openfga:import` (su sustituto es `authz:reconcile`, 3b-3).
|
|
25
33
|
*
|
|
26
34
|
* NADA del dominio está cableado: los holders llegan como `holderTypes`
|
|
27
35
|
* (morph name → tipo FGA) y los niveles de scope se derivan del propio
|
|
@@ -35,18 +43,16 @@ import type { Clock } from '../clock.js';
|
|
|
35
43
|
*/
|
|
36
44
|
export type { HolderTypeMap };
|
|
37
45
|
/**
|
|
38
|
-
* `holderTypes`
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* borra al otro (invariante 4, L0.2). El generador del modelo lo "sabía"
|
|
42
|
-
* (deduplicaba con un Set) y publicaba sin quejarse: ahora lanza aquí, al
|
|
43
|
-
* construir el driver y al generar el modelo, antes de tocar nada.
|
|
46
|
+
* La inyectividad de `holderTypes` la comprueba el módulo del modelo
|
|
47
|
+
* (`openfga_facts.ts`, compartido por los dos generadores). Se re-exporta
|
|
48
|
+
* desde aquí porque el subpath `/openfga` es la puerta publicada.
|
|
44
49
|
*/
|
|
45
|
-
export
|
|
50
|
+
export { assertHolderTypes };
|
|
46
51
|
/**
|
|
47
52
|
* Id de binding (`<scopeKey>|<uuid>`: `app|<uuid>` o `<tipo>|<uuidScope>|<uuid>`)
|
|
48
|
-
* → scope + uuid del
|
|
49
|
-
*
|
|
53
|
+
* → scope + uuid del ROL (3b-2k · K2: el `deny_binding`, que era el otro
|
|
54
|
+
* consumidor de esta gramática, se fue con el modo `resolver`; el deny es hoy
|
|
55
|
+
* una relación del scope). Se parsea DESDE LA DERECHA (3A · A1): el último componente
|
|
50
56
|
* es el uuid y el resto la clave del scope, que tiene 1 parte (`app`) o 2
|
|
51
57
|
* (`<tipo>|<uuid>`). Antes el último componente era el slug codificado
|
|
52
58
|
* (`docs~read`) y el parseo contaba partes: ambiguo en cuanto la clave del
|
|
@@ -57,7 +63,7 @@ export declare function assertHolderTypes(holderTypes: HolderTypeMap): void;
|
|
|
57
63
|
* gramática —el scope, la de identidad; el uuid, la de UUID canónico del
|
|
58
64
|
* catálogo—: un id que el driver no escribiría no es un hecho del motor
|
|
59
65
|
* aunque esté en el store. Los ids de 1.x/2.0–2.1 (con slug) caen aquí:
|
|
60
|
-
* 2.2 no los lee, y `
|
|
66
|
+
* 2.2 no los lee, y `authz:reconcile` (3b-3) los reportará como deriva.
|
|
61
67
|
* Exportada para probarla sin servidor.
|
|
62
68
|
*/
|
|
63
69
|
export declare function parseBindingId(id: string): {
|
|
@@ -74,18 +80,30 @@ export declare function parseBindingId(id: string): {
|
|
|
74
80
|
*/
|
|
75
81
|
export declare function correlateBatchResults(checks: ClientBatchCheckItem[], results: ClientBatchCheckSingleResponse[]): ClientBatchCheckSingleResponse[];
|
|
76
82
|
/**
|
|
77
|
-
*
|
|
78
|
-
* de los holders
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
83
|
+
* Crea un store nuevo + escribe el authorization model **`facts` (c2r)**
|
|
84
|
+
* derivado de los holders y de los PERMISOS del consumidor (3b-2k · K2:
|
|
85
|
+
* antes escribía el modelo del modo `resolver`, que ya no existe). Para
|
|
86
|
+
* bootstrap de un appliance o del harness de tests. El `name` lo decide el
|
|
87
|
+
* caller (el comando `openfga:provision` resuelve APP_NAME del entorno — el
|
|
88
|
+
* motor no lee env).
|
|
89
|
+
*
|
|
90
|
+
* Los permisos entran aquí porque el modelo (c2r) declara CUATRO relaciones
|
|
91
|
+
* por permiso: un store provisionado sin ellos no puede responder a ninguna
|
|
92
|
+
* pregunta. `assertFactsModelPublishable` comprueba antes las cotas (nombre
|
|
93
|
+
* de relación y techo de 262.144 bytes ⇒ 500 `E_AUTHZ_MODEL_TOO_LARGE`).
|
|
94
|
+
* Añadir un permiso al catálogo obliga a republicar el modelo: es lo que
|
|
95
|
+
* hace `syncAuthzCatalog` con la proyección inyectada, y por eso el modelo
|
|
96
|
+
* versionado del store se escribe con `--store-id`.
|
|
97
|
+
*
|
|
98
|
+
* **Y los tipos de RELACIÓN (ReBAC) van FUSIONADOS** (Fase 4-8): si el
|
|
99
|
+
* consumidor declaró `relations.config`, `openfga:provision` los pasa aquí y
|
|
100
|
+
* el modelo publicado los incluye, de modo que un store recién aprovisionado
|
|
101
|
+
* ya acepta tuplas de relación (`document#viewer`…) SIN un
|
|
102
|
+
* `authz:catalog:sync` previo. Sin ellos el modelo es facts-only y una tupla
|
|
103
|
+
* de relación es un `validation_error` del servidor («type 'document' not
|
|
104
|
+
* found»). El gate de bytes mide el modelo FUSIONADO.
|
|
87
105
|
*/
|
|
88
|
-
export declare function provisionOpenFgaStore(apiUrl: string, name: string, holderTypeMap: HolderTypeMap): Promise<{
|
|
106
|
+
export declare function provisionOpenFgaStore(apiUrl: string, name: string, holderTypeMap: HolderTypeMap, permissions: readonly string[], relations?: FactsRelationsConfig): Promise<{
|
|
89
107
|
storeId: string;
|
|
90
108
|
modelId: string;
|
|
91
109
|
}>;
|
|
@@ -96,8 +114,15 @@ export declare function provisionOpenFgaStore(apiUrl: string, name: string, hold
|
|
|
96
114
|
* "alguien escribió antes" y se propagan clasificados, con el error del SDK
|
|
97
115
|
* como causa (D6). Verificado contra OpenFGA v1.19: el duplicado llega como
|
|
98
116
|
* HTTP 400 con `apiErrorCode: 'write_failed_due_to_invalid_input'` y el
|
|
99
|
-
* mensaje "cannot write a tuple which already exists"
|
|
100
|
-
*
|
|
117
|
+
* mensaje "cannot write a tuple which already exists".
|
|
118
|
+
*
|
|
119
|
+
* **Y el 409 tiene nombre propio** (3b-2f · R3, medido contra el servidor):
|
|
120
|
+
* es el `Aborted` de un `Write` transaccional cuyas tuplas escribió otra
|
|
121
|
+
* transacción a la vez ("transactional write failed due to conflict: one or
|
|
122
|
+
* more tuples to write were inserted by another transaction"). Dice lo mismo
|
|
123
|
+
* —otro escritor llegó antes— y se trata igual: releer y re-aplicar. Lo que
|
|
124
|
+
* NO dice es QUÉ tupla chocó, así que quién existía lo decide la relectura y
|
|
125
|
+
* nunca el mensaje.
|
|
101
126
|
*/
|
|
102
127
|
export declare function isDuplicateWrite(error: unknown): boolean;
|
|
103
128
|
export interface OpenFgaDriverOptions {
|
|
@@ -169,7 +194,44 @@ export interface OpenFgaDriverOptions {
|
|
|
169
194
|
* producción lo normal es `clock` en el config del manager (`withClock`).
|
|
170
195
|
*/
|
|
171
196
|
now?: Clock;
|
|
197
|
+
/**
|
|
198
|
+
* La outbox del árbol del config (`scopes.outbox`), la MISMA instancia
|
|
199
|
+
* (3b-2d). El driver no la usa para nada: quien encola es el manager. Está
|
|
200
|
+
* aquí como EVIDENCIA del gate — un driver `facts` que se construye sin
|
|
201
|
+
* ella y sin `acceptScopeDriftRisk` es un montaje en el que un `rollback`
|
|
202
|
+
* del consumidor deja una escalada persistente e invisible (cruce 4 · S5),
|
|
203
|
+
* y eso se descubre al construir, no en la primera escritura de un tenant.
|
|
204
|
+
*/
|
|
205
|
+
outbox?: ScopeOutbox;
|
|
206
|
+
/**
|
|
207
|
+
* «Sé que sin outbox un rollback de mi transacción deja el árbol de FGA
|
|
208
|
+
* adelantado al mío, y lo asumo». Es la salida explícita del gate para
|
|
209
|
+
* quien mueve el árbol solo desde la plataforma, en un proceso que no
|
|
210
|
+
* comparte transacción con nada. Tiene que ser el booleano `true`: un
|
|
211
|
+
* valor «truthy» no es una aceptación.
|
|
212
|
+
*/
|
|
213
|
+
acceptScopeDriftRisk?: boolean;
|
|
172
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* **El gate de construcción de `facts`** (3b-2d, panel 2 cruce 4 · S5).
|
|
217
|
+
*
|
|
218
|
+
* En `hierarchy: 'facts'` el árbol vive en el store de FGA y FGA es el PDP.
|
|
219
|
+
* El consumidor notifica `scopes.moved` dentro de su transacción, el paquete
|
|
220
|
+
* escribe la arista en FGA… y si esa transacción hace `rollback` —una
|
|
221
|
+
* constraint, una validación, un timeout de pool: no hace falta un crash—
|
|
222
|
+
* la escritura de FGA NO se deshace. SQL sigue diciendo que la unit es del
|
|
223
|
+
* tenant A y FGA que es del B: todos los holders con rol en B tienen acceso
|
|
224
|
+
* a una unidad de A, y la aplicación, que lista y audita contra SQL, no
|
|
225
|
+
* puede verlo. La ventana no es un hueco entre dos operaciones: dura hasta
|
|
226
|
+
* que alguien lo descubra.
|
|
227
|
+
*
|
|
228
|
+
* Por eso `scopes.outbox` no puede ser una recomendación: una recomendación
|
|
229
|
+
* no es un mecanismo. O está el puerto, o está la firma del dueño.
|
|
230
|
+
*/
|
|
231
|
+
export declare function assertScopeDriftGuarded(options: {
|
|
232
|
+
outbox?: ScopeOutbox;
|
|
233
|
+
acceptScopeDriftRisk?: boolean;
|
|
234
|
+
}): void;
|
|
173
235
|
export declare const DEFAULT_TIMEOUT_MS = 5000;
|
|
174
236
|
/**
|
|
175
237
|
* Cota de páginas de una enumeración (1.000.000 de tuplas a 100 por página).
|
|
@@ -178,68 +240,37 @@ export declare const DEFAULT_TIMEOUT_MS = 5000;
|
|
|
178
240
|
* el deadline es por llamada (D12, auditor H7).
|
|
179
241
|
*/
|
|
180
242
|
export declare const MAX_READ_PAGES = 10000;
|
|
181
|
-
export interface ImportFactsResult {
|
|
182
|
-
/** Tuplas nuevas escritas. */
|
|
183
|
-
written: number;
|
|
184
|
-
/** Tuplas que existían con OTRA condición y se reescribieron (delete+write). */
|
|
185
|
-
updated: number;
|
|
186
|
-
/** Tuplas que ya estaban exactamente igual. */
|
|
187
|
-
unchanged: number;
|
|
188
|
-
/**
|
|
189
|
-
* Tuplas `role_binding`/`deny_binding` del store SIN correspondencia en SQL
|
|
190
|
-
* (un grant revocado en SQL, un holder que nunca estuvo, una asignación ya
|
|
191
|
-
* expirada). Solo se cuentan con `reconcile` (D14); sin `prune` siguen
|
|
192
|
-
* concediendo y el reporte lo dice.
|
|
193
|
-
*/
|
|
194
|
-
extra: number;
|
|
195
|
-
/** De las `extra`, las borradas (`prune`). En `dryRun`, las que se borrarían. */
|
|
196
|
-
deleted: number;
|
|
197
|
-
/** Asignaciones ya expiradas en SQL, no se copian. */
|
|
198
|
-
skippedExpired: number;
|
|
199
|
-
dryRun: boolean;
|
|
200
|
-
}
|
|
201
|
-
export interface ImportFactsOptions {
|
|
202
|
-
dryRun?: boolean;
|
|
203
|
-
/**
|
|
204
|
-
* Permite importar sobre un store CON tuplas: por cada hecho se lee la
|
|
205
|
-
* tupla exacta; ausente ⇒ write, presente con otra condición ⇒ delete+write
|
|
206
|
-
* (`updated`), igual ⇒ `unchanged`. Además se lee el store ENTERO
|
|
207
|
-
* (`Read({})` paginado) y lo que SQL no tiene se cuenta como `extra` (D14).
|
|
208
|
-
* Sin esto, un store no vacío es 409 `E_AUTHZ_STORE_NOT_EMPTY`.
|
|
209
|
-
*/
|
|
210
|
-
reconcile?: boolean;
|
|
211
|
-
/**
|
|
212
|
-
* Con `reconcile`: borra las tuplas `extra` (`deleted`). Es lo que hace que
|
|
213
|
-
* el reconcile CONVERJA: sin prune, un reporte de ceros no distingue "en
|
|
214
|
-
* sync" de "sobra algo que sigue concediendo". Sin `reconcile` es 500
|
|
215
|
-
* `E_AUTHZ_CONFIG`.
|
|
216
|
-
*/
|
|
217
|
-
prune?: boolean;
|
|
218
|
-
/** Reloj con el que se decide qué asignación de SQL ya expiró (`skippedExpired`). Default: la hora del proceso. */
|
|
219
|
-
now?: Clock;
|
|
220
|
-
}
|
|
221
243
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* volver a AUTHZ_DRIVER=database (solo se pierde lo escrito mientras se
|
|
227
|
-
* operó con openfga). El catálogo y la jerarquía nunca migran: son
|
|
228
|
-
* metadata local para ambos drivers.
|
|
229
|
-
* - Las asignaciones ya expiradas se saltan (no tiene sentido copiarlas);
|
|
230
|
-
* las de expiración futura viajan con la condition `not_expired`.
|
|
231
|
-
* - NUNCA `onDuplicateWrites: Ignore` (S7): en FGA la condición no es parte
|
|
232
|
-
* de la clave, así que "ignorar el duplicado" dejaba la caducidad vieja y
|
|
233
|
-
* reportaba éxito. Un store con tuplas exige `reconcile`, que compara
|
|
234
|
-
* tupla a tupla, reescribe las que difieren y cuenta las que SQL no tiene
|
|
235
|
-
* (`extra`); con `prune` las borra (`deleted`) y el reconcile converge
|
|
236
|
-
* (D14). Nunca silencioso: el reporte distingue written / updated /
|
|
237
|
-
* unchanged / extra / deleted / skippedExpired.
|
|
238
|
-
*
|
|
239
|
-
* Herramienta explícitamente de OpenFGA: los errores del SDK salen crudos.
|
|
244
|
+
* Tope de saltos al subir el árbol DEL STORE (3b-2e · E1). No es el techo de
|
|
245
|
+
* decisión —ese lo pone el servidor al evaluar (c2) y está medido en
|
|
246
|
+
* `FACTS_MAX_RESOLVE_DEPTH`—: es la red del recorrido, para que un árbol con
|
|
247
|
+
* una deriva que el anti-ciclos no vio no deje el proceso dando vueltas.
|
|
240
248
|
*/
|
|
241
|
-
export declare
|
|
249
|
+
export declare const MAX_SCOPE_CHAIN_HOPS = 1000;
|
|
242
250
|
export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
251
|
+
/**
|
|
252
|
+
* Lo que este driver declara (3b-2e · E2). Depende del MODO, así que es un
|
|
253
|
+
* getter y no un campo: una vista por prototipo (`withChainResolver`,
|
|
254
|
+
* `withClock`) declara lo mismo que su original.
|
|
255
|
+
*
|
|
256
|
+
* `roleInheritanceNative` y `listObjectsInherited` son `false` **también en
|
|
257
|
+
* `facts`**, y eso es el cruce 6 del panel: `hasRole`/`listRoles`/
|
|
258
|
+
* `listRoleScopes`/`listSubjects`/`listScopes` siguen usando `resolveChain`
|
|
259
|
+
* (en (c2) no hay alternativa, y está medido), y los `list*` enumeran con
|
|
260
|
+
* `Read` paginado, nunca con `ListObjects` (que trunca al tope del servidor
|
|
261
|
+
* sin señal). Lo único que `facts` cambia es `authorize`.
|
|
262
|
+
*/
|
|
263
|
+
get capabilities(): Readonly<{
|
|
264
|
+
hierarchyFacts: true;
|
|
265
|
+
singleCheckAuthorize: true;
|
|
266
|
+
roleInheritanceNative: false;
|
|
267
|
+
listObjectsInherited: false;
|
|
268
|
+
purgeRole: true;
|
|
269
|
+
countRoleAssignments: true;
|
|
270
|
+
canonicalScopeReads: false;
|
|
271
|
+
enumerateFacts: true;
|
|
272
|
+
transactionalWrites: false;
|
|
273
|
+
}>;
|
|
243
274
|
private client;
|
|
244
275
|
private chainResolver;
|
|
245
276
|
private holderTypes;
|
|
@@ -277,8 +308,10 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
277
308
|
private chain;
|
|
278
309
|
/** La cadena o 422: una escritura no puede ir a un scope que nadie reconoce. */
|
|
279
310
|
private knownScope;
|
|
280
|
-
/** El scope canónico para `
|
|
311
|
+
/** El scope canónico para `scopes.detached`/`purgeScope` (ver `canonicalScope`). */
|
|
281
312
|
private canonicalOrSelf;
|
|
313
|
+
/** Los destinos de un delete de hechos (`revoke`/`removeDeny`): canónico, o el fan-out de alias (3b-8 · A4). */
|
|
314
|
+
private canonicalTargets;
|
|
282
315
|
/**
|
|
283
316
|
* Vista de este driver con OTRO resolutor de ancestros y el mismo estado
|
|
284
317
|
* (cliente, memo del catálogo, deadline, diagnósticos). Es lo que usa
|
|
@@ -326,14 +359,30 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
326
359
|
* la cadena del scope del BINDING (desde él hacia la raíz).
|
|
327
360
|
*/
|
|
328
361
|
private declaredRole;
|
|
362
|
+
authorize(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<boolean>;
|
|
329
363
|
/**
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
364
|
+
* **`authorize` del modo `facts` (3b-2c): UN solo `Check`.**
|
|
365
|
+
*
|
|
366
|
+
* `can_<P>` sobre `scope:<key>` con el subject como user. El modelo (c2)
|
|
367
|
+
* ya lleva dentro las tres cosas que el modo `resolver` compone aquí:
|
|
368
|
+
* la herencia hacia abajo (`<P> from parent`), el deny explícito heredado
|
|
369
|
+
* (`denied_<P> from parent`) y la resta que hace ganar al deny
|
|
370
|
+
* (`can_<P> = <P> but not denied_<P>`). No hay `batchCheck`, no se expande
|
|
371
|
+
* la cadena y **no se llama al resolutor del consumidor** (cruce 6 del
|
|
372
|
+
* panel 2, que es también el literal que el README puede prometer).
|
|
373
|
+
*
|
|
374
|
+
* Lo único local que queda es el MEMO del catálogo, y es OBLIGATORIO: lo
|
|
375
|
+
* comprueba el llamante antes de llegar aquí. Sin esa guardia un permiso
|
|
376
|
+
* desconocido sería un `Check` de una relación que el modelo no declara —
|
|
377
|
+
* un 400 del servidor que saldría como 503— en vez del `false` que exige el
|
|
378
|
+
* invariante 5.
|
|
379
|
+
*
|
|
380
|
+
* Un scope que el árbol del consumidor no conoce no tiene tuplas en el
|
|
381
|
+
* store: responde `false` sin preguntar por él (invariante 9), pero aquí
|
|
382
|
+
* eso lo decide el propio store, no `resolveChain`. Cualquier fallo del
|
|
383
|
+
* backend sale como 503 desde el cliente envuelto; jamás un `false` mudo.
|
|
334
384
|
*/
|
|
335
|
-
private
|
|
336
|
-
authorize(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<boolean>;
|
|
385
|
+
private factsAuthorize;
|
|
337
386
|
/**
|
|
338
387
|
* `authorize` sobre N scopes con UN batchCheck (2.1, B6): los checks de
|
|
339
388
|
* todas las cadenas viajan juntos (el SDK trocea a 50 y paraleliza) y se
|
|
@@ -343,13 +392,46 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
343
392
|
* Scope desconocido o sin rol que conceda ⇒ false sin checks.
|
|
344
393
|
*/
|
|
345
394
|
authorizeMany(subject: SubjectRef, permission: string, scopes: ScopeRef[]): Promise<boolean[]>;
|
|
395
|
+
/**
|
|
396
|
+
* `authorizeMany` del modo `facts` (3b-2c): **UN `batchCheck` de N items**,
|
|
397
|
+
* uno por scope DISTINTO. En el modo `resolver` cada scope aporta los
|
|
398
|
+
* denies de su cadena más un check por (nivel, rol que concede): el lote
|
|
399
|
+
* crecía como N×M. Aquí cada scope es exactamente una pregunta,
|
|
400
|
+
* `can_<P>@scope:<key>`, y un scope repetido comparte item y respuesta
|
|
401
|
+
* (G2, CR9) en vez de duplicar el lote.
|
|
402
|
+
*
|
|
403
|
+
* Un `error` en cualquier check sigue siendo 503 entero (invariante 5, D1):
|
|
404
|
+
* lo lanza `batchCheckAll` antes de mirar un solo `allowed`.
|
|
405
|
+
*/
|
|
406
|
+
private factsAuthorizeMany;
|
|
407
|
+
/**
|
|
408
|
+
* **`{ transaction }` se rechaza también AQUÍ** (L-5, defensa en
|
|
409
|
+
* profundidad como F-05 en L-0): la puerta 1 vive en el manager, pero
|
|
410
|
+
* `manager.driver()` es la salida documentada de las barreras y por ahí un
|
|
411
|
+
* `{ transaction }` llegaría al driver; sin esta guarda el driver
|
|
412
|
+
* escribiría la tupla en el store IGNORANDO la transacción — una escritura
|
|
413
|
+
* que finge ir en tu transacción y no se deshace con tu rollback, que es
|
|
414
|
+
* exactamente el fail-open que `transactionalWrites: false` declara no
|
|
415
|
+
* poder evitar. Primera línea de las cuatro escrituras, antes de la
|
|
416
|
+
* identidad, del catálogo y del store: CERO llamadas al cliente.
|
|
417
|
+
*/
|
|
418
|
+
private rejectTransaction;
|
|
346
419
|
grant(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: GrantOptions): Promise<GrantOutcome>;
|
|
347
420
|
/**
|
|
348
|
-
* Write directo
|
|
349
|
-
*
|
|
421
|
+
* Write directo de la asignación CON su estructura (3b-2f · R3); si algo ya
|
|
422
|
+
* estaba, camino largo. Devuelve si existía la ASIGNACIÓN —lo dice la
|
|
423
|
+
* relectura, no el error: el choque puede ser de las aristas, que en (c2)
|
|
424
|
+
* las comparten todos los holders del mismo rol en el mismo scope—.
|
|
425
|
+
* Cualquier otro fallo se propaga tal cual (ya clasificado).
|
|
350
426
|
*/
|
|
351
427
|
private writeAssignment;
|
|
352
|
-
/**
|
|
428
|
+
/**
|
|
429
|
+
* delete + write (dos llamadas: FGA no admite ambas sobre la misma key en
|
|
430
|
+
* una). El write repone la ESTRUCTURA junto a la asignación: si un
|
|
431
|
+
* `purgeScope` concurrente se llevó la arista `scope#binding` entre medias,
|
|
432
|
+
* lo que queda vuelve a ser coherente en vez de una asignación inerte que
|
|
433
|
+
* `listRoles` ve y `authorize` no (3b-2f · R3).
|
|
434
|
+
*/
|
|
353
435
|
private replaceAssignment;
|
|
354
436
|
/**
|
|
355
437
|
* Estado actual de una asignación, con TRES resultados posibles y no dos.
|
|
@@ -361,10 +443,24 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
361
443
|
* que quien no pueda seguir sin él lo propague.
|
|
362
444
|
*/
|
|
363
445
|
private readAssignment;
|
|
364
|
-
revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<void>;
|
|
446
|
+
revoke(subject: SubjectRef, role: RoleQuery, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
365
447
|
hasRole(subject: SubjectRef, role: RoleQuery, scope: ScopeRef): Promise<boolean>;
|
|
366
|
-
deny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
|
|
367
|
-
removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef): Promise<void>;
|
|
448
|
+
deny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
449
|
+
removeDeny(subject: SubjectRef, permission: string, scope: ScopeRef, options?: WriteOptions): Promise<void>;
|
|
450
|
+
/**
|
|
451
|
+
* El hecho de un deny (3b-2c): una relación DEL SCOPE,
|
|
452
|
+
* `scope:<key>#denied_<P>@<holder>`. Así el modelo lo hereda hacia abajo
|
|
453
|
+
* por `parent` y `can_<P>` puede restarlo dentro del mismo `Check`
|
|
454
|
+
* (invariante 2) sin que el paquete pasee la cadena. Hasta 3b-2k · K2 el
|
|
455
|
+
* modo `resolver` lo guardaba en un objeto propio
|
|
456
|
+
* (`deny_binding:<scopeKey>|<permissionUuid>`) y el paquete expandía la
|
|
457
|
+
* cadena a un check por nivel; ese tipo se borró con el modo.
|
|
458
|
+
*
|
|
459
|
+
* La relación lleva el SLUG proyectado (no el uuid) porque el modelo la
|
|
460
|
+
* declara por nombre. El slug que llega aquí es el del catálogo: el
|
|
461
|
+
* llamante ya pasó por `findPermission`, que es quien decide qué existe.
|
|
462
|
+
*/
|
|
463
|
+
private denyTuple;
|
|
368
464
|
/**
|
|
369
465
|
* Holders con asignación vigente del rol en el scope exacto: `Read` por
|
|
370
466
|
* objeto exacto, paginado, con la caducidad filtrada en cliente. Antes era
|
|
@@ -378,6 +474,17 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
378
474
|
private listBindings;
|
|
379
475
|
/** Scopes (por clave) donde el subject tiene un deny directo del permiso (por su uuid). */
|
|
380
476
|
private deniedScopeKeys;
|
|
477
|
+
/**
|
|
478
|
+
* Los denies DIRECTOS del holder en el modo `facts`, ya traducidos a
|
|
479
|
+
* `(scope, permiso)`. No hay `deny_binding` que enumerar: se leen de una
|
|
480
|
+
* pasada las tuplas del holder sobre objetos `scope:` y se quedan las de la
|
|
481
|
+
* familia `denied_<P>`. La vuelta de relación a slug la da el CATÁLOGO —una
|
|
482
|
+
* relación que ya no declara ningún permiso no es un deny (D5), igual que
|
|
483
|
+
* un `deny_binding` de un permiso retirado—, y una clave de scope que el
|
|
484
|
+
* motor no entiende se cuenta y se registra, nunca se descarta en silencio
|
|
485
|
+
* (L0.16).
|
|
486
|
+
*/
|
|
487
|
+
private factsDenies;
|
|
381
488
|
private parseBindings;
|
|
382
489
|
private warn;
|
|
383
490
|
listRoles(subject: SubjectRef, scope: ScopeRef): Promise<string[]>;
|
|
@@ -394,23 +501,523 @@ export declare class OpenFgaAuthorizationDriver implements AuthorizationDriver {
|
|
|
394
501
|
listRoleScopes(subject: SubjectRef, scopeType: ScopeType): Promise<ScopeRef[]>;
|
|
395
502
|
listScopes(subject: SubjectRef, permission: string): Promise<ScopeRef[]>;
|
|
396
503
|
/**
|
|
397
|
-
* Denies directos del holder (2.1, B5): `Read` paginado de sus
|
|
398
|
-
* `
|
|
399
|
-
* permiso retirado no es un deny, D5), por scope exacto
|
|
400
|
-
* scopes que el árbol conoce (D8).
|
|
504
|
+
* Denies directos del holder (2.1, B5): `Read` paginado de sus relaciones
|
|
505
|
+
* `denied_<P>` sobre objetos `scope:` (nunca ListObjects, L0.7), filtrados
|
|
506
|
+
* por el catálogo (un permiso retirado no es un deny, D5), por scope exacto
|
|
507
|
+
* si se pide, y por scopes que el árbol conoce (D8).
|
|
401
508
|
*/
|
|
402
509
|
listDenies(subject: SubjectRef, scope?: ScopeRef): Promise<DenyRef[]>;
|
|
510
|
+
/**
|
|
511
|
+
* Un nodo tiene como mucho UN padre. El paquete nunca escribe dos (cada
|
|
512
|
+
* `moved` sustituye la arista entera dentro de un `Write`), así que dos
|
|
513
|
+
* padres son DERIVA: alguien más escribe en el store, y mientras tanto la
|
|
514
|
+
* herencia está trayendo hechos de dos ramas. Se lanza; no se "arregla",
|
|
515
|
+
* porque elegir cuál sobrevive sería adivinar cuál de las dos concesiones
|
|
516
|
+
* vivas es la buena (cruce 8: «si devuelve >1 ⇒ drift ⇒ lanza»).
|
|
517
|
+
*/
|
|
518
|
+
private assertOneParent;
|
|
519
|
+
/**
|
|
520
|
+
* El consumidor colgó un scope nuevo (o recolgó uno que ya existía: un
|
|
521
|
+
* `attach` sobre un nodo conocido ES un move). En modo `facts` eso es UNA
|
|
522
|
+
* tupla `scope:<hijo>#parent@scope:<padre>`; en modo `resolver` no es nada
|
|
523
|
+
* (la jerarquía la resuelve el paquete en cada pregunta).
|
|
524
|
+
*/
|
|
525
|
+
onScopeAttached(child: ScopeRef, parent: ScopeRef): Promise<void>;
|
|
526
|
+
/**
|
|
527
|
+
* El consumidor movió un scope. Procedimiento fijado en el cruce 8 del
|
|
528
|
+
* panel 2: un `Read` del padre actual —obligatorio, porque FGA rechaza
|
|
529
|
+
* borrar una tupla inexistente—, la cadena del padre NUEVO en el store
|
|
530
|
+
* (3b-8 · A5: el anti-ciclos del consumidor no ve un store
|
|
531
|
+
* desincronizado) y **UN solo `Write`** con el delete del padre viejo y el
|
|
532
|
+
* write del nuevo, que es atómico dentro de la request. O(profundidad)
|
|
533
|
+
* requests, UNA mutación.
|
|
534
|
+
*/
|
|
535
|
+
onScopeMoved(child: ScopeRef, newParent: ScopeRef): Promise<void>;
|
|
536
|
+
/**
|
|
537
|
+
* **Anti-ciclos, en el PAQUETE y antes de escribir** (cruce 3 del panel 2,
|
|
538
|
+
* bloqueante S2). Medido contra OpenFGA v1.19: el servidor ACEPTA una
|
|
539
|
+
* arista que cierra un ciclo, no se cuelga, responde en 2-7 ms y la
|
|
540
|
+
* herencia se vuelve bidireccional —un grant en un descendiente concede en
|
|
541
|
+
* el ancestro, y con la raíz dentro del ciclo concede en todo el store—.
|
|
542
|
+
* Fail-open mudo: no hay nada que capturar. La suite lo reproduce contra el
|
|
543
|
+
* `:8101` para que nadie proponga delegar esto en el backend.
|
|
544
|
+
*
|
|
545
|
+
* Las tres validaciones del cruce 8, en orden y sin escribir nada si
|
|
546
|
+
* fallan: (i) la raíz no cuelga de nadie; (ii) el padre EXISTE según el
|
|
547
|
+
* árbol del consumidor; (iii) el hijo no es ancestro-o-igual del padre.
|
|
548
|
+
* Las mismas que hace `AuthorizationManager.#assertEdge`: aquí se repiten
|
|
549
|
+
* por defensa en profundidad, porque `manager.driver()` es la salida
|
|
550
|
+
* documentada de todas las barreras del paquete.
|
|
551
|
+
*
|
|
552
|
+
* Devuelve las dos claves CANÓNICAS (invariante 17): un alias del uuid ni
|
|
553
|
+
* evade la comprobación de ciclo ni abre una segunda rama en el store.
|
|
554
|
+
*/
|
|
555
|
+
private assertEdge;
|
|
556
|
+
/**
|
|
557
|
+
* El consumidor sacó un scope del árbol. En modo `facts` se borra su
|
|
558
|
+
* arista `#parent` — y **la arista es lo ÚLTIMO** (S6, cruce 9): el
|
|
559
|
+
* manager llama primero a `purgeScope`, que borra los hechos del scope y
|
|
560
|
+
* DEMUESTRA cero o lanza (invariante 11).
|
|
561
|
+
*
|
|
562
|
+
* **El motivo del orden cambió con (c2r) y el orden NO** (3b-2i). La razón
|
|
563
|
+
* que se escribió en 3b-2b —«un scope sin ancestro dejaría de heredar los
|
|
564
|
+
* denies del padre y sus permisos serían INDENEGABLES»— ya no es cierta:
|
|
565
|
+
* ése era exactamente el 🔴 1 del auditor R2 y hoy un scope que no alcanza
|
|
566
|
+
* `app` no concede nada (`can_<P>` exige `rooted`). Lo que sigue justificando
|
|
567
|
+
* el orden es lo otro: una purga que muere a medias tiene que dejar denies
|
|
568
|
+
* de MÁS, nunca de menos, y borrar la arista antes convertiría el fallo en
|
|
569
|
+
* «se quedaron hechos vivos en un nodo que ya nadie purga» (los recoge
|
|
570
|
+
* `authz:reconcile`, pero mientras tanto el nodo es invisible para el
|
|
571
|
+
* árbol). Con la arista al final, una purga fallida se reintenta.
|
|
572
|
+
*
|
|
573
|
+
* No se tocan las aristas de los HIJOS (`scope:<hijo>#parent@scope:<este>`):
|
|
574
|
+
* el consumidor notifica un `detached` por nodo, o un `moved` para
|
|
575
|
+
* recolgarlos — y **desde (c2r) esos hijos, mientras tanto, DENIEGAN** (su
|
|
576
|
+
* cadena ya no llega a la raíz) en vez de conceder de más. Lo que quede sin
|
|
577
|
+
* nodo arriba lo ve `authz:reconcile` (3b-3).
|
|
578
|
+
*/
|
|
579
|
+
onScopeDetached(child: ScopeRef): Promise<void>;
|
|
580
|
+
/**
|
|
581
|
+
* Escribe la arista del árbol dejando UNA sola: se lee la que hay y se
|
|
582
|
+
* sustituye en el mismo `Write`. Sin diferencia no se llama al servidor
|
|
583
|
+
* (invariante 6: re-anexar al mismo padre es un no-op seguro, y además
|
|
584
|
+
* escribir una tupla que ya está sería un conflicto con los defaults
|
|
585
|
+
* estrictos del SDK).
|
|
586
|
+
*
|
|
587
|
+
* **Y el choque con otro escritor del árbol no es una caída** (3b-2h ·
|
|
588
|
+
* 🟠 4, invariante 6). Medido contra el `:8101`: dos `attached` del mismo
|
|
589
|
+
* nodo al mismo padre a la vez —lo que hacen dos pasadas del relay sobre el
|
|
590
|
+
* mismo lote, porque `pending()` no reserva nada— y el perdedor se llevaba
|
|
591
|
+
* un **503 «el backend no respondió»**, cuando el backend respondió
|
|
592
|
+
* perfectamente («cannot write a tuple which already exists»). Aquí se hace
|
|
593
|
+
* lo que el invariante 6 manda desde `grant`: releer y re-aplicar sobre lo
|
|
594
|
+
* que quedó —así el re-intento ve la arista del otro y sale por el no-op—,
|
|
595
|
+
* y una contención que no cede en `TREE_WRITE_ATTEMPTS` vueltas sale como
|
|
596
|
+
* 409 `E_AUTHZ_WRITE_CONFLICT`, nunca como un 503.
|
|
597
|
+
*
|
|
598
|
+
* Esto NO convierte dos escritores en uno: dos `attached` del mismo nodo a
|
|
599
|
+
* padres DISTINTOS siguen pudiendo dejar dos aristas (FGA no tiene
|
|
600
|
+
* compare-and-set y el `Read` de arriba es un check-then-write). Lo que
|
|
601
|
+
* impide esa carrera es el ESCRITOR ÚNICO del relay (`ScopeOutbox.acquire`).
|
|
602
|
+
*/
|
|
603
|
+
private reparent;
|
|
604
|
+
/**
|
|
605
|
+
* **El barrido del rol local** (3b-2e · E1; decisión del dueño del
|
|
606
|
+
* 2026-08-30, opción 1).
|
|
607
|
+
*
|
|
608
|
+
* En (c2) el modelo no tiene `owner`, así que `authorize` NO vuelve a
|
|
609
|
+
* decidir con el árbol de hoy si un rol LOCAL sigue siendo visible: un
|
|
610
|
+
* `role_binding` concede mientras su scope alcance al que pregunta. Sin
|
|
611
|
+
* esto, mover una unit fuera de la organización dueña de un rol local
|
|
612
|
+
* dejaría de retirar lo concedido (el invariante 18 en `database`), que es
|
|
613
|
+
* un **fail-open** — y encima uno que solo se ve comparando drivers.
|
|
614
|
+
*
|
|
615
|
+
* Lo que se toca es la arista `scope#binding`, que es lo que hace
|
|
616
|
+
* ALCANZABLE la asignación: se BORRA donde el owner del rol ya no está en
|
|
617
|
+
* la cadena y se REESCRIBE donde vuelve a estarlo (invariante 18: volver la
|
|
618
|
+
* unit a su sitio restaura). No se toca el `assignee` —el hecho de la
|
|
619
|
+
* asignación no cambia porque el árbol se mueva, igual que en `database`—
|
|
620
|
+
* ni nada de un rol GLOBAL, cuya visibilidad no depende del árbol.
|
|
621
|
+
*
|
|
622
|
+
* **Por subárbol, no por nodo** (consecuencia 2): los descendientes del
|
|
623
|
+
* nodo movido también cambian de cadena.
|
|
624
|
+
*
|
|
625
|
+
* Coste: si el catálogo no tiene NI UN rol local —el caso de todo consumidor
|
|
626
|
+
* que no usa delegación— son **cero** requests y `moved` sigue siendo el
|
|
627
|
+
* `Read` + `Write` del cruce 8. Con roles locales: una lectura por rol local
|
|
628
|
+
* (sus bindings, por `role_binding#role`), la bajada del subárbol y un
|
|
629
|
+
* `Write` por lote.
|
|
630
|
+
*/
|
|
631
|
+
private sweepLocalRoleBindings;
|
|
632
|
+
/**
|
|
633
|
+
* **El barrido por NIVEL** (3b-2g · R1; decisión del dueño del 2026-08-30
|
|
634
|
+
* (2), mismo mecanismo que E1).
|
|
635
|
+
*
|
|
636
|
+
* El modelo (c2) tampoco lleva el NIVEL (`scope_type`) del rol: la
|
|
637
|
+
* proyección dice qué permisos vincula, no en qué nivel se declara. Sin
|
|
638
|
+
* esto, cambiar el `scope_type` de un rol retira lo concedido en `database`
|
|
639
|
+
* —donde `declaredRoleAt` se evalúa en cada pregunta— y **sigue
|
|
640
|
+
* concediendo** en `facts`, que es la divergencia R1 del lote 2e.
|
|
641
|
+
*
|
|
642
|
+
* Se cierra igual que el owner: barriendo la arista `scope#binding` de los
|
|
643
|
+
* bindings de ESE rol con la regla única de visibilidad. Lo llama
|
|
644
|
+
* `projectCatalogRole`, que es el hook de «una escritura de catálogo cambió
|
|
645
|
+
* este rol»: el manager lo dispara tras `defineScopedRole`/`updateScopedRole`
|
|
646
|
+
* y un escritor «a mano» de `authz_*` tiene el mismo deber que ya tenía con
|
|
647
|
+
* el espejo de permisos (sin él, en `facts` un rol recién definido no
|
|
648
|
+
* concedería nada y quitarle un permiso seguiría concediéndolo).
|
|
649
|
+
*
|
|
650
|
+
* Coste: una lectura (los bindings del rol) y, **solo si el rol es LOCAL y
|
|
651
|
+
* tiene bindings**, la cadena del store de cada scope distinto donde cuelga
|
|
652
|
+
* uno; un `Write` por lote si hay algo que barrer. Un rol sin bindings —el
|
|
653
|
+
* caso de todo `defineScopedRole`— son 0 escrituras.
|
|
654
|
+
*/
|
|
655
|
+
private sweepRoleVisibility;
|
|
656
|
+
/**
|
|
657
|
+
* La arista `scope#binding` de un binding, a escribir o a borrar según la
|
|
658
|
+
* **regla única de visibilidad** (`declaredRoleAt`, la misma que evalúa
|
|
659
|
+
* `database` en cada pregunta): el rol tiene que estar declarado para el
|
|
660
|
+
* NIVEL de ese scope (3b-2g · R1) y ser global o tener a su owner en la
|
|
661
|
+
* cadena (3b-2e · E1). Visible ⇒ la arista se (re)escribe; no visible ⇒ se
|
|
662
|
+
* borra. El `assignee` no se toca: la asignación existe igual, lo que
|
|
663
|
+
* cambia es dónde se la ve.
|
|
664
|
+
*/
|
|
665
|
+
private classifyBindingEdge;
|
|
666
|
+
/**
|
|
667
|
+
* Aplica el barrido en lotes ≤ 100 (el límite del `Write`).
|
|
668
|
+
*
|
|
669
|
+
* `Ignore` en las dos direcciones: el barrido dice el estado que DEBE
|
|
670
|
+
* quedar, no el delta — borrar lo que ya no está y reescribir lo que ya
|
|
671
|
+
* estaba son no-ops, no errores (invariante 6).
|
|
672
|
+
*/
|
|
673
|
+
private applyBindingSweep;
|
|
674
|
+
/**
|
|
675
|
+
* `[key, ...ancestros]` según el ÁRBOL DEL STORE (3b-2e · E1), subiendo por
|
|
676
|
+
* `scope#parent`. Es la cadena con la que FGA va a decidir, que es la que
|
|
677
|
+
* tiene que gobernar el barrido; el resolutor del consumidor no participa.
|
|
678
|
+
* Un nodo con más de un padre es deriva y se dice (`assertOneParent`), y el
|
|
679
|
+
* recorrido está acotado por el mismo tope de páginas que las
|
|
680
|
+
* enumeraciones: un ciclo escrito a mano no cuelga el proceso.
|
|
681
|
+
*/
|
|
682
|
+
private storeChain;
|
|
403
683
|
/**
|
|
404
684
|
* Purga del scope exacto en FGA (N7, S6, B2). No hay "borrar todo lo de
|
|
405
685
|
* este objeto": se leen por objeto EXACTO los bindings posibles — un
|
|
406
|
-
* `role_binding` por cada rol del catálogo de ese `scope_type`
|
|
407
|
-
* `
|
|
408
|
-
* trunca sin avisar, L0.7), se borra
|
|
409
|
-
* se vuelve a leer cada objeto: si
|
|
410
|
-
*
|
|
411
|
-
*
|
|
686
|
+
* `role_binding` por cada rol del catálogo de ese `scope_type` — más el
|
|
687
|
+
* objeto `scope:<key>` (donde viven los `denied_<P>` y el `#binding`),
|
|
688
|
+
* paginando `Read` (nunca ListObjects: trunca sin avisar, L0.7), se borra
|
|
689
|
+
* en lotes ≤ 100 (límite del Write) y se vuelve a leer cada objeto: si
|
|
690
|
+
* queda algo, se lanza. Un rol retirado del catálogo deja bindings
|
|
691
|
+
* inalcanzables por esta vía; es el precio de no tener un índice por
|
|
692
|
+
* objeto, y lo vigilará `authz:reconcile` (3b).
|
|
412
693
|
*/
|
|
413
694
|
purgeScope(purged: ScopeRef): Promise<void>;
|
|
695
|
+
/**
|
|
696
|
+
* La **proyección derivada del catálogo** en el store (3b-2a · A5; regla
|
|
697
|
+
* del catálogo reescrita, panel 2 cruce 7). Se pasa a `syncAuthzCatalog`,
|
|
698
|
+
* que la usa en dos momentos: comprueba que el catálogo que va a quedar es
|
|
699
|
+
* publicable (cotas de nombre y techo del modelo) ANTES de escribir, y
|
|
700
|
+
* rehace las tuplas `role:<uuid>#permits_<P>@<holder>:*` con el catálogo ya
|
|
701
|
+
* confirmado.
|
|
702
|
+
*
|
|
703
|
+
* Sigue sin ser el catálogo: es un espejo reconstruible que ningún camino
|
|
704
|
+
* de LECTURA de este driver consulta para responder qué permisos tiene un
|
|
705
|
+
* rol (A6). Quien decide es `authz_*` a través del memo.
|
|
706
|
+
*/
|
|
707
|
+
catalogProjection(): CatalogProjection;
|
|
708
|
+
/**
|
|
709
|
+
* **El marcador de raíz de (c2r)** (3b-2i): `scope:app#rooted@<holder>:*`,
|
|
710
|
+
* una tupla por holder type en todo el store y CERO por scope.
|
|
711
|
+
*
|
|
712
|
+
* Va aquí —en la proyección del catálogo, o sea en cada `syncAuthzCatalog`—
|
|
713
|
+
* y no en `attached`, porque el evento que hace falta cubrir es **añadir un
|
|
714
|
+
* holderType al `config`**: sin esto ese holder denegaría en TODO el store
|
|
715
|
+
* aunque el modelo se haya republicado, que es la única forma realista de
|
|
716
|
+
* quedarse sin marcador en un store vivo. Es idempotente: un `Read` y, solo
|
|
717
|
+
* si falta algo, un `Write` con lo que falta (0 escrituras en el caso
|
|
718
|
+
* normal).
|
|
719
|
+
*
|
|
720
|
+
* Solo en modo `facts`: el modelo del modo `resolver` no declara `rooted` y
|
|
721
|
+
* escribirlo sería un 400 del servidor.
|
|
722
|
+
*
|
|
723
|
+
* ⚠️ Sin marcador el store entero DENIEGA (fail-closed, medido). Por eso se
|
|
724
|
+
* repone en cada sync y `authz:reconcile` (3b-3) tiene el deber escrito de
|
|
725
|
+
* reportarlo como deriva cuando falte.
|
|
726
|
+
*/
|
|
727
|
+
private ensureFactsRoot;
|
|
728
|
+
/**
|
|
729
|
+
* Espeja los vínculos rol→permiso: escribe lo que falta y BORRA lo que
|
|
730
|
+
* sobra, en UN `Write` por lote con deletes y writes juntos (cruce 8:
|
|
731
|
+
* queda prohibido el patrón `deleteTuples()` + `writeTuples()`, que no es
|
|
732
|
+
* atómico). Con (c2) quitar un permiso de un rol son tantos deletes como
|
|
733
|
+
* holders y ninguna reescritura del modelo.
|
|
734
|
+
*
|
|
735
|
+
* Sin diferencias no se llama al servidor: un `sync` que no cambió el
|
|
736
|
+
* catálogo escribe CERO tuplas.
|
|
737
|
+
*/
|
|
738
|
+
private projectCatalog;
|
|
739
|
+
/**
|
|
740
|
+
* **Purga un ROL con sus hechos** (3b-2e · E4; hasta aquí este driver no lo
|
|
741
|
+
* traía y por eso `defineScopedRole` era 500 `E_AUTHZ_UNSUPPORTED` antes de
|
|
742
|
+
* escribir, 3E · P4).
|
|
743
|
+
*
|
|
744
|
+
* Lo que lo hace posible es (c2): el binding APUNTA A SU ROL
|
|
745
|
+
* (`role_binding:…#role@role:<uuid>`), así que los bindings de un rol se
|
|
746
|
+
* enumeran filtrando por `user` — y con la arista `scope#binding` se sabe de
|
|
747
|
+
* qué scope cuelga cada uno. En el modo `resolver` esas dos aristas no
|
|
748
|
+
* existen y el método TAMPOCO: el constructor lo retira (el manager lo lee
|
|
749
|
+
* como «no sé purgar» y se niega antes de escribir).
|
|
750
|
+
*
|
|
751
|
+
* Orden: **hechos primero, catálogo después** (el mismo de `detached`, S6).
|
|
752
|
+
* No hay transacción que abarque FGA y SQL, así que lo que se garantiza es
|
|
753
|
+
* la dirección segura: mientras la fila del rol siga viva, lo que quede en
|
|
754
|
+
* el store es visible y reintentable; al revés quedarían hechos huérfanos
|
|
755
|
+
* que resucitarían al recrear el slug. Y se DEMUESTRA cero (invariante 11):
|
|
756
|
+
* si algo sobrevive, 500 `E_AUTHZ_PURGE_INCOMPLETE` y el catálogo no se
|
|
757
|
+
* toca.
|
|
758
|
+
*/
|
|
759
|
+
purgeRole(roleUuid: string): Promise<void>;
|
|
760
|
+
/**
|
|
761
|
+
* Cuántos hechos VIGENTES tiene cada rol, en todos los scopes (3b-2j).
|
|
762
|
+
*
|
|
763
|
+
* Aquí los hechos son TUPLAS, no filas: `role_binding:<scope>|<rol>#assignee@<holder>`,
|
|
764
|
+
* con la caducidad en su *condition*. Por eso esta pregunta es del PUERTO
|
|
765
|
+
* y no del barrido: hasta 3b-2j `pruneOrphanRoles` contaba
|
|
766
|
+
* `authz_assignments` —la tabla del driver `database`, vacía con este— y
|
|
767
|
+
* el `stillGranting` que se lee justo antes de purgar decía SIEMPRE «este
|
|
768
|
+
* rol no concede», sobre roles que concedían (medido en el lote 2i).
|
|
769
|
+
*
|
|
770
|
+
* Lo hace posible lo mismo que hace posible `purgeRole`: el binding apunta
|
|
771
|
+
* a su rol (`role_binding:…#role@role:<uuid>`, (c2)), así que los bindings
|
|
772
|
+
* de un rol se enumeran filtrando por `user`. En modo `resolver` esa arista
|
|
773
|
+
* no existe y el método TAMPOCO (el constructor lo retira, y el manager lo
|
|
774
|
+
* lee como «no lo sé»).
|
|
775
|
+
*
|
|
776
|
+
* La arista estructural se lee con `includeExpired` —no caduca, la
|
|
777
|
+
* caducidad está en el `assignee`— y los assignees sin él: la caducidad es
|
|
778
|
+
* ESTRICTA y con el reloj del driver, igual que en `authorize`. Un `user`
|
|
779
|
+
* que no se entiende como holder se cuenta igual (a diferencia de
|
|
780
|
+
* `listSubjects`, que lo descarta): aquí contar de MÁS es el lado seguro —
|
|
781
|
+
* marca el rol para que un humano lo mire— y contar de menos es decir «no
|
|
782
|
+
* concede» sobre algo que sí.
|
|
783
|
+
*
|
|
784
|
+
* Coste: por rol preguntado, una lectura de sus bindings más una por
|
|
785
|
+
* binding. Lo llama `pruneOrphanRoles` con los HUÉRFANOS de la pasada (no
|
|
786
|
+
* con el catálogo entero) y corre en un comando de plataforma, no en el
|
|
787
|
+
* camino de una petición.
|
|
788
|
+
*/
|
|
789
|
+
countRoleAssignments(roleUuids: string[]): Promise<number[]>;
|
|
790
|
+
/**
|
|
791
|
+
* **Rehace la proyección de UN rol** (3b-2e · E4). En (c2) lo que un rol
|
|
792
|
+
* concede son tuplas (`role:<uuid>#permits_<P>@<holder>:*`), no el catálogo
|
|
793
|
+
* local: una escritura de catálogo que no las toque deja un rol que no
|
|
794
|
+
* concede nada (`defineScopedRole`) o que sigue concediendo lo que ya no
|
|
795
|
+
* vincula (`updateScopedRole`) — lo segundo es un fail-open. `syncAuthzCatalog`
|
|
796
|
+
* ya lo hace para el catálogo entero cuando el consumidor le pasa la
|
|
797
|
+
* proyección; esto es lo mismo para las escrituras de la API de delegación,
|
|
798
|
+
* y cuesta una lectura por holder.
|
|
799
|
+
*
|
|
800
|
+
* **Y son DOS proyecciones, no una** (3b-2g · R1): lo que el rol concede
|
|
801
|
+
* (`permits_<P>`) y **dónde es visible** (`scope#binding`, `sweepRoleVisibility`).
|
|
802
|
+
* El modelo (c2) no lleva el NIVEL del rol, así que un `scope_type` que
|
|
803
|
+
* cambia sin barrer deja la asignación concediendo en un nivel que el
|
|
804
|
+
* catálogo ya no declara — retirado en `database` y vivo aquí, que es la
|
|
805
|
+
* divergencia R1 del lote 2e.
|
|
806
|
+
*/
|
|
807
|
+
projectCatalogRole(roleUuid: string): Promise<void>;
|
|
808
|
+
/**
|
|
809
|
+
* **Reconstruye el store desde `authz_*` + el árbol del consumidor.**
|
|
810
|
+
*
|
|
811
|
+
* Es la razón de ser de la fase («todo en un driver o todo en otro, con una
|
|
812
|
+
* migración idempotente y bidireccional») y la ÚNICA primitiva de migración
|
|
813
|
+
* del paquete: `openfga:import` se borró en 3b-2k · K2 porque escribía las
|
|
814
|
+
* tuplas de un modelo que ya no existe.
|
|
815
|
+
*
|
|
816
|
+
* Migra las TRES cosas que hacen completo a este driver:
|
|
817
|
+
* 1. el **marcador de raíz** (`scope:app#rooted@<holder>:*`, 3b-2i) — sin
|
|
818
|
+
* él el store entero DENIEGA, así que va primero;
|
|
819
|
+
* 2. la **proyección del catálogo** (`role:<uuid>#permits_<P>`), leída con
|
|
820
|
+
* la MISMA función que usa `syncAuthzCatalog` (`readCatalogProjectionSnapshot`),
|
|
821
|
+
* para que reconcile no "arregle" en cada pasada lo que el sync deja bien;
|
|
822
|
+
* 3. el **árbol** (`scope#parent`) desde `scopes.enumerateEdges`, y
|
|
823
|
+
* 4. los **hechos**: `authz_assignments` (assignee + las dos aristas de
|
|
824
|
+
* (c2)) y `authz_denies` (`scope#denied_<P>`).
|
|
825
|
+
*
|
|
826
|
+
* **Qué borra y qué no.** Lo DERIVADO —marcador, catálogo y árbol— es un
|
|
827
|
+
* espejo de datos locales que nadie más escribe: lo que sobra se borra
|
|
828
|
+
* siempre (cruce 9 · S7 lo exige para las aristas que `enumerateEdges` no
|
|
829
|
+
* respalda, y es lo que repara un nodo con DOS padres, 3b-2h · 🟠 4). Los
|
|
830
|
+
* HECHOS solo se borran con `prune`: son irreversibles y su origen depende
|
|
831
|
+
* de qué driver esté vivo. La excepción, a propósito, es la arista
|
|
832
|
+
* `scope#binding` de una asignación que el origen SÍ respalda pero cuya
|
|
833
|
+
* regla de visibilidad dice que NO (invariante 18): dejarla es fail-OPEN
|
|
834
|
+
* —es justo la escritura que `scopes.moved`/`projectCatalogRole` pudieron
|
|
835
|
+
* perder si el relay no pasó—, así que se borra siempre y se cuenta en
|
|
836
|
+
* `drift.roleVisibility`.
|
|
837
|
+
*
|
|
838
|
+
* **Nada de `Ignore` a ciegas** (cruce 9 · S7): el importador viejo escribía
|
|
839
|
+
* con `onDuplicateWrites: Ignore` y por eso una tupla que ya estaba con OTRA
|
|
840
|
+
* caducidad se quedaba como estaba y encima se contaba como escrita —rompía
|
|
841
|
+
* los invariantes 3 y 6—. Aquí el estado del destino se LEE entero antes de
|
|
842
|
+
* decidir, la diferencia de caducidad se resuelve con delete + write, y los
|
|
843
|
+
* contadores salen del diff, no del write. `Ignore` se conserva solo como
|
|
844
|
+
* red contra una carrera (y por eso la migración va con `manager.freeze()`).
|
|
845
|
+
*
|
|
846
|
+
* `dryRun` es el VERIFICADOR: mismo recorrido, cero escrituras, mismos
|
|
847
|
+
* números. Read-only por contrato (cruce 4 · S18): **un `--fix` está
|
|
848
|
+
* prohibido** y no se implementa ni se deja preparado.
|
|
849
|
+
*/
|
|
850
|
+
reconcile(source: ReconcileSource, options?: ReconcileOptions): Promise<ReconcileReport>;
|
|
851
|
+
/**
|
|
852
|
+
* Las relaciones `permits_<P>` que DECLARA el modelo publicado del store, o
|
|
853
|
+
* `null` si no se puede saber (un store sin modelo). No es una barrera de
|
|
854
|
+
* seguridad: es la diferencia entre contar un permiso que este store no
|
|
855
|
+
* puede llevar y morirse con un 400 a mitad de la migración.
|
|
856
|
+
*/
|
|
857
|
+
private modelPermissions;
|
|
858
|
+
/**
|
|
859
|
+
* El árbol del ORIGEN, paginado (`scopes.enumerateEdges`), con los ciclos
|
|
860
|
+
* apartados. Un ciclo no se escribe NUNCA: FGA lo evalúa y la herencia pasa
|
|
861
|
+
* a ser bidireccional (un grant en un descendiente concede en el ancestro,
|
|
862
|
+
* cruce 3), así que aquí sale como reporte y sus nodos se quedan sin arista
|
|
863
|
+
* —o sea, sin `rooted`, o sea denegando (fail-closed)—.
|
|
864
|
+
*/
|
|
865
|
+
private readSourceTree;
|
|
866
|
+
/**
|
|
867
|
+
* Los HECHOS del origen (`authz_assignments` y `authz_denies`), leídos **por
|
|
868
|
+
* lotes con cursor** sobre la clave primaria: una pasada interrumpida se
|
|
869
|
+
* repite y converge (es idempotente), y una base grande no entra entera en
|
|
870
|
+
* memoria de golpe. Los DOS barridos van en la MISMA transacción de lectura
|
|
871
|
+
* repetible (3b-6, `withSourceSnapshot`): sueltos, componían dos mitades de
|
|
872
|
+
* dos operaciones distintas y FABRICABAN un permiso.
|
|
873
|
+
*
|
|
874
|
+
* Cada fila que NO se migra sale contada y con su motivo:
|
|
875
|
+
* - `unknown-scope`: el árbol del consumidor ya no resuelve ese scope
|
|
876
|
+
* (`detached` de un ancestro). Sus tuplas del store son las de la
|
|
877
|
+
* «resurrección» (3b-0b · AA4) y las borra `--prune`.
|
|
878
|
+
* - `unknown-role` / `unknown-permission`: el catálogo ya no lo declara
|
|
879
|
+
* (un rol retirado; invariante 11 dice que los recoge este comando).
|
|
880
|
+
* - `expired`: la asignación ya no concede; migrarla sería escribir una
|
|
881
|
+
* caducidad pasada.
|
|
882
|
+
* - `role-not-visible`: la asignación existe (y `listRoles`/`hasRole` la
|
|
883
|
+
* enumeran), pero el rol NO es visible en ese scope con el árbol y el
|
|
884
|
+
* catálogo de HOY (invariante 18), así que su arista `scope#binding` no
|
|
885
|
+
* se escribe — y si el store la tiene, se borra.
|
|
886
|
+
* - `unknown-holder-type`: el `holderTypes` del config no declara ese
|
|
887
|
+
* morph name, así que no hay usuario FGA que escribir.
|
|
888
|
+
*/
|
|
889
|
+
private readSourceFacts;
|
|
890
|
+
/**
|
|
891
|
+
* **La foto CONSISTENTE del origen** (3b-6; 🔴 3 del panel 3, verificado en
|
|
892
|
+
* el código por el juez).
|
|
893
|
+
*
|
|
894
|
+
* `readSourceFacts` recorre `authz_assignments` y DESPUÉS `authz_denies`.
|
|
895
|
+
* Cada barrido se construía sobre la conexión global, así que entre los dos
|
|
896
|
+
* había un hueco —el tiempo de pasear la primera tabla entera por lotes de
|
|
897
|
+
* 100: segundos o minutos en una base real— por el que se colaban
|
|
898
|
+
* operaciones de negocio COMPUESTAS. Un offboarding es `revoke` +
|
|
899
|
+
* `removeDeny`: si cae ahí, la pasada se queda con la mitad de cada una y
|
|
900
|
+
* escribe en el destino **el rol sin su deny**, o sea un permiso que NI el
|
|
901
|
+
* estado anterior NI el posterior concedían, con el reporte diciendo
|
|
902
|
+
* `written=13 extra=0 skipped={} clean=true`. No es una pérdida: es una
|
|
903
|
+
* ESCALADA fabricada, y el operador no tiene ni un motivo para desconfiar
|
|
904
|
+
* del verde.
|
|
905
|
+
*
|
|
906
|
+
* Con los dos barridos dentro de UNA transacción de lectura repetible, el
|
|
907
|
+
* peor resultado de la ventana deja de ser «un estado que nunca existió» y
|
|
908
|
+
* pasa a ser «el estado consistente de `t0`»: la deriva recuperable que
|
|
909
|
+
* repite la pasada siguiente.
|
|
910
|
+
*
|
|
911
|
+
* **Qué garantiza cada motor, porque no es lo mismo** (Fase 2.5 dixit):
|
|
912
|
+
* - **PostgreSQL**: `BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ`.
|
|
913
|
+
* La foto se toma en la primera sentencia de la transacción y no se
|
|
914
|
+
* mueve; garantía del motor.
|
|
915
|
+
* - **MySQL/InnoDB**: `SET TRANSACTION ISOLATION LEVEL REPEATABLE READ` +
|
|
916
|
+
* `BEGIN`, y la lectura consistente queda fijada en la primera consulta.
|
|
917
|
+
* REPEATABLE READ es su default, pero se DECLARA a propósito: el default
|
|
918
|
+
* es config del servidor y no una promesa del paquete.
|
|
919
|
+
* - **SQLite**: no acepta nivel de aislamiento —knex avisa y lo ignora—,
|
|
920
|
+
* así que no se le pide: su transacción de LECTURA ya es una foto (en
|
|
921
|
+
* WAL el lector conserva su snapshot mientras un escritor confirma; sin
|
|
922
|
+
* WAL el escritor espera al lector). Por eso aquí se pregunta el
|
|
923
|
+
* dialecto en vez de mandar el nivel a ciegas.
|
|
924
|
+
*
|
|
925
|
+
* **Lo que esto NO cubre y queda declarado**: cubre la dirección cuyo
|
|
926
|
+
* origen es `authz_*` (`--to=openfga`). Cuando la fuente de verdad de los
|
|
927
|
+
* hechos es el STORE (`readOriginFacts` → `enumerateFacts`: `--to=database`
|
|
928
|
+
* y la pasada de mantenimiento) las páginas de `Read` **tampoco son una
|
|
929
|
+
* foto consistente** y no hay REPEATABLE READ que valga: ahí la misma
|
|
930
|
+
* composición de media transacción sigue siendo posible. La instrumentación
|
|
931
|
+
* posible en esa dirección es `readChanges({ startTime })` —detectar y
|
|
932
|
+
* nombrar la tupla, no prevenirla—, y no está hecha.
|
|
933
|
+
*/
|
|
934
|
+
private withSourceSnapshot;
|
|
935
|
+
/**
|
|
936
|
+
* **Los hechos por el PUERTO** (3b-5): la otra fuente de verdad posible.
|
|
937
|
+
*
|
|
938
|
+
* Se usa cuando `authz_*` NO manda —el caso que faltaba: el motor ya sirve
|
|
939
|
+
* desde este driver y sus hechos son las tuplas del store—, y también sirve
|
|
940
|
+
* para migrar desde otro driver que sepa `enumerateFacts`. El recorrido y
|
|
941
|
+
* los motivos son **los mismos** que los de `readSourceFacts` (`expired`,
|
|
942
|
+
* `unknown-scope`, `unknown-role`, `unknown-holder-type`,
|
|
943
|
+
* `role-not-visible`): lo único que cambia es de dónde salen las filas, que
|
|
944
|
+
* es justo la decisión que no se estaba tomando.
|
|
945
|
+
*
|
|
946
|
+
* Consecuencia buscada: con el store como origen, `wanted` describe lo que
|
|
947
|
+
* el store YA tiene, así que la pasada no escribe ni borra un solo hecho —y
|
|
948
|
+
* sí rehace lo DERIVADO (marcador, catálogo, árbol) y aplica el barrido de
|
|
949
|
+
* visibilidad del invariante 18 con el árbol y el catálogo de HOY, que es
|
|
950
|
+
* la reparación que el invariante promete y no existía.
|
|
951
|
+
*
|
|
952
|
+
* Disciplina del cursor idéntica a la de `--to=database`: como mucho
|
|
953
|
+
* `limit` por página, el cursor tiene que AVANZAR y la cota `maxTuples` se
|
|
954
|
+
* aplica también al origen.
|
|
955
|
+
*/
|
|
956
|
+
private readOriginFacts;
|
|
957
|
+
/**
|
|
958
|
+
* Una ASIGNACIÓN del origen (venga de `authz_*` o del puerto) traducida a
|
|
959
|
+
* lo que el store debe tener: el `assignee` con su caducidad, la arista
|
|
960
|
+
* `role_binding#role` y —solo si el rol es VISIBLE ahí— la `scope#binding`.
|
|
961
|
+
* Es la única implementación de esa regla en esta dirección: tenerla dos
|
|
962
|
+
* veces era tenerla distinta según de dónde salieran los hechos.
|
|
963
|
+
*/
|
|
964
|
+
private wantAssignment;
|
|
965
|
+
/** Un DENY del origen (permiso ya como slug) traducido a `scope#denied_<P>`. */
|
|
966
|
+
private wantDeny;
|
|
967
|
+
/**
|
|
968
|
+
* Pasea una tabla `authz_*` por lotes de `batchSize` con cursor sobre
|
|
969
|
+
* `uuid` (clave primaria: orden total y estable). Es lo que hace la pasada
|
|
970
|
+
* REANUDABLE y lo que impide que una base grande entre entera de golpe.
|
|
971
|
+
*
|
|
972
|
+
* **El cliente llega por parámetro** (3b-6): antes cada página se construía
|
|
973
|
+
* sobre el `db` global, así que dos barridos consecutivos eran dos fotos
|
|
974
|
+
* distintas. Quien decide la foto es `withSourceSnapshot`, y aquí solo se
|
|
975
|
+
* obedece — un call-site nuevo que pase `db` vuelve a abrir el hueco, y por
|
|
976
|
+
* eso el parámetro es obligatorio y no tiene default.
|
|
977
|
+
*/
|
|
978
|
+
private eachRow;
|
|
979
|
+
/**
|
|
980
|
+
* **Los hechos de este store, paginados** (3b-3b): la mitad ORIGEN de la
|
|
981
|
+
* migración, la que hace posible `authz:reconcile --to=database`.
|
|
982
|
+
*
|
|
983
|
+
* Se lee con `Read` y su `continuation_token`, no con `ListObjects`: el
|
|
984
|
+
* plan lo dice con esas palabras y el motivo es que `ListObjects` **no
|
|
985
|
+
* tiene `continuation_token`** —corta al tope del servidor sin señal
|
|
986
|
+
* (S16)—, así que no hay forma de pasear un store entero con él. `Read`
|
|
987
|
+
* sin filtro es además lo único que ve la basura de una versión anterior,
|
|
988
|
+
* que es lo que sale en `skipped`.
|
|
989
|
+
*
|
|
990
|
+
* **Nada se filtra**: una asignación caducada sale con su `expiresAt` para
|
|
991
|
+
* que el DESTINO la cuente con su motivo. Si se filtrara aquí desaparecería
|
|
992
|
+
* sin dejar rastro en ningún contador, que es exactamente lo que una
|
|
993
|
+
* migración no puede hacer. Por lo mismo NO interviene el reloj del driver.
|
|
994
|
+
*
|
|
995
|
+
* Lo que no es un hecho —la estructura de (c2) (`parent`, `binding`,
|
|
996
|
+
* `role`, `rooted`) y la proyección del catálogo (`permits_<P>`)— no se
|
|
997
|
+
* emite ni se cuenta: es DERIVADO, se rehace desde el catálogo y el árbol
|
|
998
|
+
* del consumidor, y en esta dirección ni siquiera se migra. Lo que sí se
|
|
999
|
+
* cuenta es lo que TENDRÍA que ser un hecho y no se entiende.
|
|
1000
|
+
*/
|
|
1001
|
+
enumerateFacts(page: {
|
|
1002
|
+
limit: number;
|
|
1003
|
+
after?: string;
|
|
1004
|
+
}): Promise<ReconcileFactPage>;
|
|
1005
|
+
/**
|
|
1006
|
+
* El store ENTERO, con la caducidad de cada tupla. Un `Read` sin filtro es
|
|
1007
|
+
* la única forma de ver lo que SOBRA —incluida la basura de una versión
|
|
1008
|
+
* anterior, cuyos tipos el modelo de hoy ni declara (se lee y se borra, se
|
|
1009
|
+
* comprobó contra el servidor)—: filtrando por objeto solo se ve lo que ya
|
|
1010
|
+
* se sabe que existe. Paginado y acotado como todas las enumeraciones.
|
|
1011
|
+
*/
|
|
1012
|
+
private readEveryTuple;
|
|
1013
|
+
/**
|
|
1014
|
+
* Aplica el plan en lotes ≤ 100 (el límite del `Write`). `Ignore` en las dos
|
|
1015
|
+
* direcciones es RED, no política: el plan sale de un diff sobre el estado
|
|
1016
|
+
* leído y la migración corre con las escrituras congeladas, así que un
|
|
1017
|
+
* duplicado o un borrado que ya no está solo puede venir de una carrera —y
|
|
1018
|
+
* en una carrera es preferible seguir a abortar la migración entera—.
|
|
1019
|
+
*/
|
|
1020
|
+
private applyReconcileWrites;
|
|
414
1021
|
/**
|
|
415
1022
|
* TODAS las tuplas que casan con el filtro, paginando `Read` hasta agotar
|
|
416
1023
|
* el `continuation_token`, sin las caducadas. Es la única primitiva de
|