@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import db from '@adonisjs/lucid/services/db';
|
|
2
2
|
import { v7 as uuidv7 } from 'uuid';
|
|
3
3
|
import { APP_SCOPE_TYPE } from '../types.js';
|
|
4
|
-
import { assertCatalogUuid, assertIdentity, assertScope, chainKeysFrom, normalizeRoleQuery } from '../identity.js';
|
|
4
|
+
import { assertCatalogUuid, assertIdentity, assertScope, assertSnapshotReads, chainKeysFrom, normalizeRoleQuery } from '../identity.js';
|
|
5
5
|
import { resolveGrantExpiry, sameInstant } from '../expiry.js';
|
|
6
6
|
import { AuthorizationBackendError, AuthorizationConfigError, AuthorizationInternalError, InvalidIdentityError, MassReconcileRefusedError, ReconcileTooLargeError, RoleNotVisibleError, UnknownPermissionError, UnknownRoleError, UnsupportedOperationError, WriteConflictError, } from '../errors.js';
|
|
7
7
|
import { RECONCILE_MAX_DETAILS, emptyReconcilePhases, reconcileBatchSize, reconcileMaxTuples, sumReconcilePhases, } from '../reconcile.js';
|
|
@@ -243,6 +243,12 @@ export function whereScopeIn(query, column, scopes, intent) {
|
|
|
243
243
|
}
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Las partes posibles de la FOTO de una decisión (rotación 2 · cierre 3, `readFoto`): `grant` = asignaciones que
|
|
248
|
+
* conceden UN permiso (con el vínculo rol→permiso por `JOIN`), `role` = asignaciones sin filtrar por permiso
|
|
249
|
+
* (`rolesInChain`) y `deny` = denies. El literal de `foto_kind` sale de esta tabla CERRADA, jamás de un dato.
|
|
250
|
+
*/
|
|
251
|
+
const FOTO_KINDS = { grant: 'grant', deny: 'deny', role: 'role' };
|
|
246
252
|
export const DEFAULT_TIMEOUT_MS = 5_000;
|
|
247
253
|
/**
|
|
248
254
|
* Driver `database`: motor de autorización propio sobre las tablas `authz_*`.
|
|
@@ -262,7 +268,7 @@ export class DatabaseAuthorizationDriver {
|
|
|
262
268
|
/**
|
|
263
269
|
* Lo que este driver declara (3b-2e · E2). El árbol es del consumidor
|
|
264
270
|
* (`resolveChain`), así que no hay hechos de jerarquía ni deriva que
|
|
265
|
-
* mitigar; `authorize`
|
|
271
|
+
* mitigar; `authorize` es UNA consulta SQL de hechos (la foto, rotación 2 · cierre 3) más el catálogo y el árbol, no un `Check`; la membresía la resuelve
|
|
266
272
|
* el propio SQL contra la cadena que le pasan; los `list*` no enumeran
|
|
267
273
|
* herencia (invariante 7); y `purgeRole` está implementado en una
|
|
268
274
|
* transacción.
|
|
@@ -327,12 +333,27 @@ export class DatabaseAuthorizationDriver {
|
|
|
327
333
|
// `resolveChain` y la barrera del freeze (autoridad). Un despliegue con
|
|
328
334
|
// pool 1 lo declara `false` (ver `DatabaseDriverOptions`).
|
|
329
335
|
transactionalWrites: this.transactionalWrites,
|
|
336
|
+
// Estrategia 2.5.0 · firma 4 (D15): `false` EXPLÍCITO — `database` es el driver ESTABLE de 2.5.0 (el experimental
|
|
337
|
+
// es `openfga`). Explícito para que la copia de `authorization.capabilities()` y este objeto digan lo mismo.
|
|
338
|
+
experimental: false,
|
|
339
|
+
// Rotación 2 · cierre 3 (D15, L12): toda decisión que combina concesión y deny se lee en UNA sentencia
|
|
340
|
+
// (`readFoto`), y `readSnapshot` da esa foto a las composiciones del manager.
|
|
341
|
+
snapshotDecisions: true,
|
|
330
342
|
});
|
|
331
343
|
assertCatalogOptions('DatabaseAuthorizationDriver', options);
|
|
332
344
|
this.catalog =
|
|
333
345
|
options.catalog ??
|
|
334
346
|
new CatalogCache({ driver: 'database', timeoutMs: this.timeoutMs, revalidate: options.catalogRevalidate });
|
|
335
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* **El catálogo de una ESCRITURA** (K4; asentar · cierre 2, D-1): con la versión contrastada en fresco SIEMPRE
|
|
350
|
+
* (`CatalogCache.freshView`), ignore o no la ventana `{ everyMs }`. Un `catalog` inyectado que no lo traiga cae en
|
|
351
|
+
* `view()` (el contrato del memo es suyo). Gemelo de `OpenFgaAuthorizationDriver.writeCatalog`.
|
|
352
|
+
*/
|
|
353
|
+
writeCatalog() {
|
|
354
|
+
const catalog = this.catalog;
|
|
355
|
+
return typeof catalog.freshView === 'function' ? catalog.freshView() : catalog.view();
|
|
356
|
+
}
|
|
336
357
|
/**
|
|
337
358
|
* Vista de este driver con OTRO resolutor de la cadena y el mismo estado
|
|
338
359
|
* (conexión, memo del catálogo, deadline). Es lo que usa
|
|
@@ -395,6 +416,54 @@ export class DatabaseAuthorizationDriver {
|
|
|
395
416
|
const rows = await this.sql(operation, () => fn().limit(1));
|
|
396
417
|
return rows[0] ?? null;
|
|
397
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* **La FOTO de una decisión: el punto de paso ÚNICO** (rotación 2 · cierre 3; 4.ª aparición del linaje L12 «lector
|
|
421
|
+
* rasgado», la primera en `database`). Propiedad universal: *toda DECISIÓN de `database` que combina concesión y deny
|
|
422
|
+
* se lee en UNA foto*. Hasta aquí `authorize` leía los denies y las asignaciones en DOS sentencias, y una escritura
|
|
423
|
+
* que caía entre las dos daba una respuesta que NINGÚN estado real daba (un `deny` y después un `grant` ⇒ `true` una
|
|
424
|
+
* vez; repro `.claude/reproducciones/database-authorize-rasgado.spec.ts`).
|
|
425
|
+
*
|
|
426
|
+
* La foto es **UNA sentencia**: las partes van unidas con `UNION ALL` y cada fila dice de qué parte es
|
|
427
|
+
* (`foto_kind`). Una sentencia ve UN estado en los tres motores y en cualquier aislamiento —PostgreSQL toma la foto
|
|
428
|
+
* por SENTENCIA en READ COMMITTED, InnoDB una vista de lectura por sentencia, SQLite una transacción de lectura
|
|
429
|
+
* implícita—, así que no hace falta abrir una transacción REPEATABLE READ como `withSourceSnapshot` del reconcile:
|
|
430
|
+
* eso costaría BEGIN/COMMIT por pregunta en el camino caliente y, con un pool de 1 (`:memory:`), retendría la única
|
|
431
|
+
* conexión mientras el catálogo o un `resolveChain` SQL del consumidor esperan otra. Elegido por eso: una ida y
|
|
432
|
+
* vuelta (antes dos), sin conexión retenida y sin nivel por motor.
|
|
433
|
+
*
|
|
434
|
+
* Lo que va DENTRO de la foto: los HECHOS (asignaciones —con el vínculo rol→permiso por `JOIN` cuando la parte lo
|
|
435
|
+
* filtra— y denies). Lo que va FUERA, antes o después: el catálogo (memo contrastado con la versión compartida,
|
|
436
|
+
* invariante 14) y la cadena del consumidor (`resolveChain`). «Cadena frente a hechos» (un `scopes.moved` entre el
|
|
437
|
+
* `resolveChain` y la foto) depende del resolutor del consumidor y está DECLARADO en README «Limits» (fila «Torn
|
|
438
|
+
* read»), no se arregla aquí.
|
|
439
|
+
*
|
|
440
|
+
* Columnas comunes de cada parte: `foto_kind`, `foto_scope_type`, `foto_scope_uuid`, `foto_role` y
|
|
441
|
+
* `foto_permission` (`null` en la parte que no la tiene). Solo SQL estándar del query builder (M6). Una sola parte
|
|
442
|
+
* es una sola `SELECT`, sin `UNION`. La guardia `database.decision.snapshot` cuenta las sentencias que tocan
|
|
443
|
+
* `authz_assignments`/`authz_denies` por decisión: con dos, rojo.
|
|
444
|
+
*/
|
|
445
|
+
async readFoto(operation, parts) {
|
|
446
|
+
const foto = { grant: [], deny: [], role: [] };
|
|
447
|
+
const live = parts.filter((part) => part !== null);
|
|
448
|
+
if (live.length === 0)
|
|
449
|
+
return foto;
|
|
450
|
+
const selects = live.map((part) => part.query.select(db.raw(`'${FOTO_KINDS[part.kind]}' as foto_kind`), `${part.scope}_type as foto_scope_type`, `${part.scope}_uuid as foto_scope_uuid`, part.role ? `${part.role} as foto_role` : db.raw('null as foto_role'), part.permission ? `${part.permission} as foto_permission` : db.raw('null as foto_permission')));
|
|
451
|
+
const [head, ...rest] = selects;
|
|
452
|
+
const rows = await this.sql(operation, () => (rest.length ? head.unionAll(rest) : head));
|
|
453
|
+
for (const row of rows) {
|
|
454
|
+
const kind = row.foto_kind;
|
|
455
|
+
if (!(kind in foto)) {
|
|
456
|
+
throw new AuthorizationInternalError(`database.${operation}: la foto devolvió una fila de una parte desconocida (${String(kind)})`);
|
|
457
|
+
}
|
|
458
|
+
foto[kind].push({
|
|
459
|
+
scope_type: row.foto_scope_type,
|
|
460
|
+
scope_uuid: row.foto_scope_uuid,
|
|
461
|
+
role_uuid: row.foto_role ?? null,
|
|
462
|
+
permission_uuid: row.foto_permission ?? null,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return foto;
|
|
466
|
+
}
|
|
398
467
|
/**
|
|
399
468
|
* Por dónde escribe `operation` (L-3): la transacción ABIERTA del llamante
|
|
400
469
|
* si llegó en `options.transaction` —validada contra la conexión de este
|
|
@@ -499,8 +568,6 @@ export class DatabaseAuthorizationDriver {
|
|
|
499
568
|
.where('permission_uuid', perm.uuid), 'scope', chain, 'read');
|
|
500
569
|
if (!deniedQuery)
|
|
501
570
|
return false;
|
|
502
|
-
if (await this.first('authorize.denies', () => deniedQuery))
|
|
503
|
-
return false;
|
|
504
571
|
// Una asignación vigente, en la cadena, cuyo rol concede el permiso Y es
|
|
505
572
|
// visible en su nivel (global u owner ancestro-o-igual del scope de la
|
|
506
573
|
// asignación, 3B · B2). `assignableAt`/`rank` no entran: composición.
|
|
@@ -513,8 +580,13 @@ export class DatabaseAuthorizationDriver {
|
|
|
513
580
|
.where('a.holder_uuid', subject.uuid), chain);
|
|
514
581
|
if (!grantedQuery)
|
|
515
582
|
return false;
|
|
516
|
-
|
|
517
|
-
|
|
583
|
+
// Los denies y las asignaciones en UNA foto (rotación 2 · cierre 3, L12): deny en la cadena ⇒ false; si no, una
|
|
584
|
+
// asignación vigente que concede ⇒ true. Nunca dos sentencias: ver `readFoto`.
|
|
585
|
+
const foto = await this.readFoto('authorize', [
|
|
586
|
+
{ kind: 'deny', query: deniedQuery, scope: 'scope', permission: 'permission_uuid' },
|
|
587
|
+
{ kind: 'grant', query: this.whereActive(grantedQuery, 'a.expires_at'), scope: 'a.scope', role: 'a.role_uuid' },
|
|
588
|
+
]);
|
|
589
|
+
return foto.deny.length === 0 && foto.grant.length > 0;
|
|
518
590
|
}
|
|
519
591
|
async grant(subject, role, scope, options = {}) {
|
|
520
592
|
assertIdentity({ subject, role, scope, expiresAt: options.expiresAt });
|
|
@@ -610,7 +682,11 @@ export class DatabaseAuthorizationDriver {
|
|
|
610
682
|
// a lo sumo uno es visible ahí y los demás serían filas huérfanas; el
|
|
611
683
|
// scope puede no existir ya para el árbol (D8), así que no se filtra por
|
|
612
684
|
// visibilidad — quitar nunca concede.
|
|
613
|
-
|
|
685
|
+
// Asentar · cierre 2 (D-1 del re-ataque; gemelo del 🟡 5 de `openfga`, K4): el catálogo de una ESCRITURA, con la
|
|
686
|
+
// versión en fresco. Con el memo de `{ everyMs }` un local recién definido en otro proceso que ensombrece al global
|
|
687
|
+
// no salía en `rolesToRevoke`: el DELETE no tocaba su fila y la asignación local seguía concediendo (un «revocado»
|
|
688
|
+
// falso, mudo y persistente).
|
|
689
|
+
const named = rolesToRevoke(await this.writeCatalog(), role, scope);
|
|
614
690
|
// Sin cadena con la que canonizar, TODAS las ortografías de las que el
|
|
615
691
|
// uuid puede ser alias (3b-8 · A4, mismo fan-out que `scopes.detached`):
|
|
616
692
|
// con una sola, un alias hacía del DELETE un no-op silencioso y la fila
|
|
@@ -768,13 +844,21 @@ export class DatabaseAuthorizationDriver {
|
|
|
768
844
|
assertIdentity({ subject });
|
|
769
845
|
for (const scope of chain)
|
|
770
846
|
assertScope(scope);
|
|
847
|
+
const foto = await this.readFoto('rolesInChain', [this.rolesInChainPart(subject, chain)]);
|
|
848
|
+
return this.rolesInChainFrom(foto.role, chain);
|
|
849
|
+
}
|
|
850
|
+
/** La parte de la foto de `rolesInChain`: asignaciones vigentes del holder en los scopes de la cadena (`null` sin cadena). */
|
|
851
|
+
rolesInChainPart(subject, chain) {
|
|
771
852
|
const query = whereScopeIn(db
|
|
772
853
|
.from('authz_assignments as a')
|
|
773
854
|
.where('a.holder_type', subject.type)
|
|
774
855
|
.where('a.holder_uuid', subject.uuid), 'a.scope', chain, 'read');
|
|
775
856
|
if (!query)
|
|
776
|
-
return
|
|
777
|
-
|
|
857
|
+
return null;
|
|
858
|
+
return { kind: 'role', query: this.whereActive(query, 'a.expires_at'), scope: 'a.scope', role: 'a.role_uuid' };
|
|
859
|
+
}
|
|
860
|
+
/** De las filas de la foto a la respuesta de `rolesInChain` (catálogo DESPUÉS de la foto: es memo, no hechos). */
|
|
861
|
+
async rolesInChainFrom(rows, chain) {
|
|
778
862
|
const catalog = await this.catalog.view();
|
|
779
863
|
const keysFrom = chainKeysFrom(chain);
|
|
780
864
|
const levelOf = new Map(chain.map((s, i) => [dbScopeKey(s), i]));
|
|
@@ -837,25 +921,38 @@ export class DatabaseAuthorizationDriver {
|
|
|
837
921
|
const perm = await this.findPermission(permission);
|
|
838
922
|
if (!perm)
|
|
839
923
|
return [];
|
|
840
|
-
|
|
924
|
+
// Las asignaciones que conceden y los denies del permiso en UNA foto (rotación 2 · cierre 3, L12): antes eran dos
|
|
925
|
+
// sentencias y un offboarding (revoke, luego removeDeny) entre ellas listaba un scope que ningún estado concedía.
|
|
926
|
+
const foto = await this.readFoto('listScopes', [this.grantsOfPart(subject, perm.uuid), this.deniesPart(subject, perm.uuid)]);
|
|
927
|
+
return this.listScopesFrom(foto.grant, foto.deny);
|
|
928
|
+
}
|
|
929
|
+
/** La parte «asignaciones vigentes del holder cuyo rol concede el permiso», en todo scope (la de `listScopes`). */
|
|
930
|
+
grantsOfPart(subject, permissionUuid) {
|
|
931
|
+
const query = this.whereActive(db
|
|
841
932
|
.from('authz_assignments as a')
|
|
842
933
|
.join('authz_role_permissions as rp', 'rp.role_uuid', 'a.role_uuid')
|
|
843
|
-
.where('rp.permission_uuid',
|
|
934
|
+
.where('rp.permission_uuid', permissionUuid)
|
|
844
935
|
.where('a.holder_type', subject.type)
|
|
845
|
-
.where('a.holder_uuid', subject.uuid), 'a.expires_at')
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
936
|
+
.where('a.holder_uuid', subject.uuid), 'a.expires_at');
|
|
937
|
+
return { kind: 'grant', query, scope: 'a.scope', role: 'a.role_uuid', permission: 'rp.permission_uuid' };
|
|
938
|
+
}
|
|
939
|
+
/** La parte «denies del holder» (de UN permiso si se da; si no, todos), en todo scope. */
|
|
940
|
+
deniesPart(subject, permissionUuid) {
|
|
941
|
+
const query = db.from('authz_denies').where('holder_type', subject.type).where('holder_uuid', subject.uuid);
|
|
942
|
+
if (permissionUuid !== undefined)
|
|
943
|
+
query.where('permission_uuid', permissionUuid);
|
|
944
|
+
return { kind: 'deny', query, scope: 'scope', permission: 'permission_uuid' };
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* De la foto a la respuesta de `listScopes` (árbol y catálogo DESPUÉS de la foto). `denies` son los del permiso: por
|
|
948
|
+
* scope, el conjunto de roles que conceden; el scope se lista si el árbol lo conoce, alguno de esos roles existe ahí
|
|
949
|
+
* (3B · B2) y ningún deny de la cadena lo bloquea — exactamente lo que `authorize` diría.
|
|
950
|
+
*/
|
|
951
|
+
async listScopesFrom(grants, denies) {
|
|
852
952
|
const deniedKeys = new Set(denies.map((d) => `${d.scope_type}:${fromDbScopeUuid(d.scope_uuid) ?? ''}`));
|
|
853
|
-
// Por scope: el conjunto de roles que conceden; el scope se lista si el
|
|
854
|
-
// árbol lo conoce, alguno de esos roles existe ahí (3B · B2) y ningún
|
|
855
|
-
// deny de la cadena lo bloquea — exactamente lo que `authorize` diría.
|
|
856
953
|
const catalog = await this.catalog.view();
|
|
857
954
|
const byScope = new Map();
|
|
858
|
-
for (const row of
|
|
955
|
+
for (const row of grants) {
|
|
859
956
|
const k = rowScopeKey(row);
|
|
860
957
|
if (!byScope.has(k))
|
|
861
958
|
byScope.set(k, { scope: { type: row.scope_type, uuid: fromDbScopeUuid(row.scope_uuid) }, roles: [] });
|
|
@@ -894,7 +991,11 @@ export class DatabaseAuthorizationDriver {
|
|
|
894
991
|
const query = chain ? whereScopeIn(base, 'scope', [chain[0]], 'read') : base;
|
|
895
992
|
if (!query)
|
|
896
993
|
return [];
|
|
897
|
-
const
|
|
994
|
+
const foto = await this.readFoto('listDenies', [{ kind: 'deny', query, scope: 'scope', permission: 'permission_uuid' }]);
|
|
995
|
+
return this.deniesFrom(foto.deny, scope === undefined);
|
|
996
|
+
}
|
|
997
|
+
/** De las filas de denies a `DenyRef[]` (catálogo y, sin `scope` pedido, árbol DESPUÉS de la foto). */
|
|
998
|
+
async deniesFrom(rows, resolveEach) {
|
|
898
999
|
const view = await this.catalog.view();
|
|
899
1000
|
const result = [];
|
|
900
1001
|
for (const row of rows) {
|
|
@@ -902,12 +1003,42 @@ export class DatabaseAuthorizationDriver {
|
|
|
902
1003
|
if (!permission)
|
|
903
1004
|
continue;
|
|
904
1005
|
const denyScope = { type: row.scope_type, uuid: fromDbScopeUuid(row.scope_uuid) };
|
|
905
|
-
if (
|
|
1006
|
+
if (resolveEach && !(await this.chain(denyScope, 'listDenies')))
|
|
906
1007
|
continue;
|
|
907
1008
|
result.push({ permission, scope: denyScope });
|
|
908
1009
|
}
|
|
909
1010
|
return result;
|
|
910
1011
|
}
|
|
1012
|
+
/**
|
|
1013
|
+
* **Varias lecturas del puerto en UNA foto** (rotación 2 · cierre 3, L12; método OPCIONAL del puerto,
|
|
1014
|
+
* `AuthorizationDriver.readSnapshot`, capacidad `snapshotDecisions`). Es lo que el manager usa para las decisiones
|
|
1015
|
+
* que COMPONE con dos métodos del puerto —`effectivePermissions` y la policy de delegación (`rolesInChain` +
|
|
1016
|
+
* `listDenies`), `authorizedScopes` (`listScopes` + `listDenies`)—: cada respuesta pedida es EXACTAMENTE la del método
|
|
1017
|
+
* homónimo, y todas salen de la MISMA foto de los hechos (`readFoto`: una sentencia). Lo no pedido no viene.
|
|
1018
|
+
* Árbol y catálogo se leen después de la foto, como en cada método suelto.
|
|
1019
|
+
*/
|
|
1020
|
+
async readSnapshot(subject, reads) {
|
|
1021
|
+
assertIdentity({ subject });
|
|
1022
|
+
const asked = assertSnapshotReads(reads);
|
|
1023
|
+
const chain = asked.rolesInChain;
|
|
1024
|
+
const perm = asked.listScopes !== undefined ? await this.findPermission(asked.listScopes) : null;
|
|
1025
|
+
// Los denies: todos si se pidió `listDenies`; si solo hace falta para `listScopes`, los de ese permiso.
|
|
1026
|
+
const wantsDenies = asked.listDenies === true || perm !== null;
|
|
1027
|
+
const foto = await this.readFoto('readSnapshot', [
|
|
1028
|
+
chain ? this.rolesInChainPart(subject, chain) : null,
|
|
1029
|
+
perm ? this.grantsOfPart(subject, perm.uuid) : null,
|
|
1030
|
+
wantsDenies ? this.deniesPart(subject, asked.listDenies === true ? undefined : perm.uuid) : null,
|
|
1031
|
+
]);
|
|
1032
|
+
const answer = {};
|
|
1033
|
+
if (chain)
|
|
1034
|
+
answer.rolesInChain = await this.rolesInChainFrom(foto.role, chain);
|
|
1035
|
+
if (asked.listScopes !== undefined) {
|
|
1036
|
+
answer.listScopes = perm ? await this.listScopesFrom(foto.grant, foto.deny.filter((d) => d.permission_uuid === perm.uuid)) : [];
|
|
1037
|
+
}
|
|
1038
|
+
if (asked.listDenies === true)
|
|
1039
|
+
answer.listDenies = await this.deniesFrom(foto.deny, true);
|
|
1040
|
+
return answer;
|
|
1041
|
+
}
|
|
911
1042
|
/**
|
|
912
1043
|
* Borra asignaciones y denies del scope exacto, en una transacción. Si el
|
|
913
1044
|
* árbol aún lo conoce se purga su identidad canónica (K1); si ya no (el
|
|
@@ -1141,6 +1272,11 @@ export class DatabaseAuthorizationDriver {
|
|
|
1141
1272
|
scope_type: target.type,
|
|
1142
1273
|
scope_uuid: toDbScopeUuid(target),
|
|
1143
1274
|
};
|
|
1275
|
+
// Cierre-2 de L11 (C1): un deny que el origen guarda con una condición ajena se migra como deny (fail-closed:
|
|
1276
|
+
// perderlo concedería) pero se DECLARA: `invalid-condition` es deriva y el operador decide.
|
|
1277
|
+
if (fact.invalidCondition !== undefined) {
|
|
1278
|
+
note('deny', 'invalid-condition', `${fact.detail} (${fact.invalidCondition})`);
|
|
1279
|
+
}
|
|
1144
1280
|
usableFacts += 1;
|
|
1145
1281
|
const denyId = denyKey(row);
|
|
1146
1282
|
if (wantedDenies.has(denyId))
|
|
@@ -1309,4 +1445,3 @@ export class DatabaseAuthorizationDriver {
|
|
|
1309
1445
|
}
|
|
1310
1446
|
}
|
|
1311
1447
|
}
|
|
1312
|
-
//# sourceMappingURL=database_driver.js.map
|
|
@@ -88,6 +88,13 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
|
|
|
88
88
|
deny(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
|
|
89
89
|
/** `removeDeny`: borra la tupla `denied_<rel>`. Sobre CUALQUIER tipo declarado (D8/R2-4 b). No-op si no está. */
|
|
90
90
|
removeDeny(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationDenyOptions): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* **El techo de anidación de grupos de ESTE driver** (lote «antes de RC» · R1, decisión del dueño 2026-10-17 (5)):
|
|
93
|
+
* `database` no tiene techo técnico (la CTE deduplica y termina con ciclos), así que es `null` SALVO el opt-in
|
|
94
|
+
* explícito `database.enforceGroupDepth: true`, que aplica el `maxGroupDepth` de la config (obligatorio y numérico
|
|
95
|
+
* con el opt-in: lo valida `defineRelationsConfig` al arrancar).
|
|
96
|
+
*/
|
|
97
|
+
groupDepthLimit(): Promise<number | null>;
|
|
91
98
|
unrelate(subject: RelSubject, relation: string, object: RelObject, partition: ScopeRef, options?: RelationWriteOptions): Promise<void>;
|
|
92
99
|
/**
|
|
93
100
|
* **F-05 en las LECTURAS y las PURGAS del driver** (2.4.0-alpha.3, cierre
|
|
@@ -117,14 +124,14 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
|
|
|
117
124
|
* misma función que el doble y `openfga` cuando alinea su `batchCheck`). Tope, `[]` y F-05 por ítem como `checkMany`.
|
|
118
125
|
*/
|
|
119
126
|
decideMany(subject: RelSubject, relation: string, objects: RelObject[], partition: ScopeRef): Promise<RelationDecision[]>;
|
|
120
|
-
listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef
|
|
121
|
-
listSubjects(relation: string, object: RelObject, partition: ScopeRef
|
|
127
|
+
listObjects(subject: RelSubject, relation: string, objectType: string, partition: ScopeRef): Promise<RelationObjectsPage>;
|
|
128
|
+
listSubjects(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
122
129
|
/**
|
|
123
130
|
* **`listDenies`** (lote D · D1): los sujetos con deny DIRECTO de `relation` sobre `object` —hechos, como
|
|
124
131
|
* `listSubjects` sobre la relación almacenada `denied_<rel>`; no resta ni deriva—. Sobre CUALQUIER tipo declarado
|
|
125
132
|
* (D8/R2-4 b: con el opt-in retirado, los denies vivos se siguen pudiendo listar y borrar).
|
|
126
133
|
*/
|
|
127
|
-
listDenies(relation: string, object: RelObject, partition: ScopeRef
|
|
134
|
+
listDenies(relation: string, object: RelObject, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
128
135
|
/**
|
|
129
136
|
* **`denyPhase` — ¿admite este driver escrituras de deny en el tipo?** (lote D · D2, R2-5): la fase del opt-in EN
|
|
130
137
|
* LA CONFIG DE ESTE DRIVER (`'write'`, `'read'` o `null`). F-05 estricto del par (422), cero sentencias: es
|
|
@@ -140,7 +147,7 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
|
|
|
140
147
|
* directos): en un grupo con `member(g1#member, g2)`, `membersOf(g2)` trae a
|
|
141
148
|
* los holders de g1 y `listSubjects(member, g2)` solo el userset `g1#member`.
|
|
142
149
|
*/
|
|
143
|
-
membersOf(object: RelObject, relation: string, partition: ScopeRef
|
|
150
|
+
membersOf(object: RelObject, relation: string, partition: ScopeRef): Promise<RelationSubjectsPage>;
|
|
144
151
|
purgeObject(object: RelObject, partition: ScopeRef, options?: RelationTransactionOptions): Promise<void>;
|
|
145
152
|
purgeSubject(subject: RelSubject, partition: ScopeRef, options?: RelationTransactionOptions): Promise<void>;
|
|
146
153
|
/**
|
|
@@ -184,4 +191,3 @@ export declare class DatabaseRelationsDriver implements RelationsDriver {
|
|
|
184
191
|
*/
|
|
185
192
|
enumerateRelationsMany(partitions: ScopeRef[], page?: RelationPage): Promise<RelationTuplePage>;
|
|
186
193
|
}
|
|
187
|
-
//# sourceMappingURL=database_relations_driver.d.ts.map
|