@jantstack/adonis-authz 1.1.0 → 2.0.0-alpha.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/LICENSE +1 -1
- package/README.md +417 -58
- package/build/commands/authz_catalog_diff.d.ts +28 -0
- package/build/commands/authz_catalog_diff.d.ts.map +1 -0
- package/build/commands/authz_catalog_diff.js +67 -0
- package/build/commands/authz_catalog_diff.js.map +1 -0
- package/build/commands/authz_catalog_sync.d.ts +20 -0
- package/build/commands/authz_catalog_sync.d.ts.map +1 -0
- package/build/commands/authz_catalog_sync.js +58 -0
- package/build/commands/authz_catalog_sync.js.map +1 -0
- package/build/commands/main.d.ts +2 -0
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +2 -0
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_import.d.ts +8 -2
- package/build/commands/openfga_import.d.ts.map +1 -1
- package/build/commands/openfga_import.js +29 -6
- package/build/commands/openfga_import.js.map +1 -1
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +2 -1
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/index.d.ts +45 -6
- package/build/index.d.ts.map +1 -1
- package/build/index.js +37 -4
- package/build/index.js.map +1 -1
- package/build/src/catalog.d.ts +249 -2
- package/build/src/catalog.d.ts.map +1 -1
- package/build/src/catalog.js +709 -59
- package/build/src/catalog.js.map +1 -1
- package/build/src/catalog_cache.d.ts +300 -0
- package/build/src/catalog_cache.d.ts.map +1 -0
- package/build/src/catalog_cache.js +656 -0
- package/build/src/catalog_cache.js.map +1 -0
- package/build/src/clock.d.ts +24 -0
- package/build/src/clock.d.ts.map +1 -0
- package/build/src/clock.js +7 -0
- package/build/src/clock.js.map +1 -0
- package/build/src/define_config.d.ts +102 -6
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/backend_guard.d.ts +92 -0
- package/build/src/drivers/backend_guard.d.ts.map +1 -0
- package/build/src/drivers/backend_guard.js +221 -0
- package/build/src/drivers/backend_guard.js.map +1 -0
- package/build/src/drivers/database_driver.d.ts +242 -15
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +692 -127
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/openfga_driver.d.ts +341 -27
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +1020 -271
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/sql_expiry.d.ts +53 -0
- package/build/src/drivers/sql_expiry.d.ts.map +1 -0
- package/build/src/drivers/sql_expiry.js +66 -0
- package/build/src/drivers/sql_expiry.js.map +1 -0
- package/build/src/errors.d.ts +333 -0
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +352 -0
- package/build/src/errors.js.map +1 -1
- package/build/src/expiry.d.ts +27 -0
- package/build/src/expiry.d.ts.map +1 -0
- package/build/src/expiry.js +50 -0
- package/build/src/expiry.js.map +1 -0
- package/build/src/hierarchical_resolver.d.ts +56 -0
- package/build/src/hierarchical_resolver.d.ts.map +1 -0
- package/build/src/hierarchical_resolver.js +87 -0
- package/build/src/hierarchical_resolver.js.map +1 -0
- package/build/src/identity.d.ts +155 -0
- package/build/src/identity.d.ts.map +1 -0
- package/build/src/identity.js +359 -0
- package/build/src/identity.js.map +1 -0
- package/build/src/manager.d.ts +225 -7
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +1670 -23
- package/build/src/manager.js.map +1 -1
- package/build/src/memoize_ancestors.d.ts +23 -0
- package/build/src/memoize_ancestors.d.ts.map +1 -0
- package/build/src/memoize_ancestors.js +42 -0
- package/build/src/memoize_ancestors.js.map +1 -0
- package/build/src/middleware/app_access_middleware.d.ts +8 -6
- package/build/src/middleware/app_access_middleware.d.ts.map +1 -1
- package/build/src/middleware/app_access_middleware.js +10 -26
- package/build/src/middleware/app_access_middleware.js.map +1 -1
- package/build/src/models/authz_assignment.d.ts +5 -5
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +5 -5
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +11 -5
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_permission.js +4 -0
- package/build/src/models/authz_permission.js.map +1 -1
- package/build/src/models/authz_role.d.ts +13 -6
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role.js +6 -1
- package/build/src/models/authz_role.js.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +5 -5
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/openfga.d.ts +13 -0
- package/build/src/openfga.d.ts.map +1 -0
- package/build/src/openfga.js +12 -0
- package/build/src/openfga.js.map +1 -0
- package/build/src/sql_descendants.d.ts +51 -0
- package/build/src/sql_descendants.d.ts.map +1 -0
- package/build/src/sql_descendants.js +129 -0
- package/build/src/sql_descendants.js.map +1 -0
- package/build/src/testing/contract.d.ts +138 -7
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +2946 -24
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +4 -2
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +2 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/scope_tree.d.ts +65 -0
- package/build/src/testing/scope_tree.d.ts.map +1 -0
- package/build/src/testing/scope_tree.js +145 -0
- package/build/src/testing/scope_tree.js.map +1 -0
- package/build/src/traits/authz_scopes.d.ts +30 -6
- package/build/src/traits/authz_scopes.d.ts.map +1 -1
- package/build/src/traits/authz_scopes.js +30 -18
- package/build/src/traits/authz_scopes.js.map +1 -1
- package/build/src/traits/has_uuid.d.ts +5 -5
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +530 -28
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +10 -4
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/app_acl.stub +4 -2
- package/build/stubs/config/authorization.stub +66 -12
- package/build/stubs/migration.stub +57 -13
- package/package.json +11 -6
package/build/src/types.js
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* suite corre contra cada driver — es el juez del contrato):
|
|
13
13
|
*
|
|
14
14
|
* 1. **Scopes jerárquicos, herencia SOLO hacia abajo.** La cadena de un scope
|
|
15
|
-
* es `[scope, ...ancestros]` (`app` es la raíz: su cadena es él
|
|
15
|
+
* es `[scope canónico, ...ancestros]` (`app` es la raíz: su cadena es él
|
|
16
|
+
* mismo); la identidad de un scope es la que devuelve el resolutor, nunca
|
|
17
|
+
* la forma con la que lo escribió el llamante (2.5-B · K1).
|
|
16
18
|
* Un grant en un scope autoriza en ese scope y en TODOS sus descendientes;
|
|
17
19
|
* nunca en hermanos ni ancestros. En T1 solo opera `app` (los ancestros de
|
|
18
|
-
* `organization`/`unit` se completan en T2/T3 vía `
|
|
20
|
+
* `organization`/`unit` se completan en T2/T3 vía `resolveChain`).
|
|
19
21
|
* 2. **Deny explícito gana.** Un deny de un permiso en cualquier scope de la
|
|
20
22
|
* cadena bloquea `authorize`, aunque un rol lo conceda (el deny también
|
|
21
23
|
* hereda hacia abajo). Quitar el deny restaura el permiso.
|
|
@@ -28,8 +30,12 @@
|
|
|
28
30
|
* holder sin asignación vigente → `false`, nunca throw en `authorize`.
|
|
29
31
|
* En cambio `grant`/`deny` con rol/permiso fuera del catálogo → throw
|
|
30
32
|
* (error de programación, no de autorización).
|
|
31
|
-
* 6. **Idempotencia de escritura.** Re-`grant` no duplica (
|
|
32
|
-
* `
|
|
33
|
+
* 6. **Idempotencia de escritura.** Re-`grant` no duplica (`expiresAt` en
|
|
34
|
+
* tres estados: omitido preserva, `null` quita, `Date` fija);
|
|
35
|
+
* re-`revoke`/re-`deny`/re-`removeDeny` son no-ops seguros.
|
|
36
|
+
* 7. **El árbol es un hecho del contrato.** Un scope que el resolutor no
|
|
37
|
+
* conoce (`null`) deniega, no lista y no admite escrituras; `purgeScope`
|
|
38
|
+
* borra los hechos del scope exacto (los del catálogo) y demuestra cero.
|
|
33
39
|
*/
|
|
34
40
|
/** Nombre reservado del scope raíz. */
|
|
35
41
|
export const APP_SCOPE_TYPE = 'app';
|
package/build/src/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAoBH,uCAAuC;AACvC,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAA;AAQnC,sDAAsD;AACtD,MAAM,CAAC,MAAM,SAAS,GAAa,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA"}
|
|
@@ -17,9 +17,11 @@ import type { CatalogSpec } from '@jantstack/adonis-authz'
|
|
|
17
17
|
* Para el ACL POR TENANT ver `config/organization_acl.ts` (migra al motor
|
|
18
18
|
* en la tanda 2).
|
|
19
19
|
*
|
|
20
|
-
* Workflow para añadir un permiso:
|
|
20
|
+
* Workflow para añadir o quitar un permiso:
|
|
21
21
|
* 1. Editar este archivo.
|
|
22
|
-
* 2. `node ace
|
|
22
|
+
* 2. `node ace authz:catalog:sync` (quitar un permiso de un rol lo retira
|
|
23
|
+
* de verdad: el sync poda los vínculos que ya no listas).
|
|
24
|
+
* 3. En CI, `node ace authz:catalog:diff` falla si la base no coincide.
|
|
23
25
|
*/
|
|
24
26
|
const appAclConfig = {
|
|
25
27
|
/**
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
exports({ to: app.configPath('authorization.ts') })
|
|
3
3
|
}}}
|
|
4
4
|
import env from '#start/env'
|
|
5
|
-
import { defineConfig, DatabaseAuthorizationDriver } from '@jantstack/adonis-authz'
|
|
5
|
+
import { defineConfig, DatabaseAuthorizationDriver, APP_SCOPE } from '@jantstack/adonis-authz'
|
|
6
6
|
import type { AuthzWriteEvent, ScopeRef } from '@jantstack/adonis-authz'
|
|
7
|
+
import { appAclCatalog } from '#config/app_acl'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Holders de tu aplicación: morph name (`@MorphMap` del modelo) → tipo del
|
|
@@ -17,16 +18,40 @@ export const holderTypes = {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
* Jerarquía de scopes de tu dominio
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Jerarquía de scopes de tu dominio: la ÚNICA costura entre tu dominio y el
|
|
22
|
+
* motor. El motor solo conoce la raíz (`app`); si tienes niveles propios
|
|
23
|
+
* (organizaciones, proyectos, sedes…), devuelve aquí la CADENA CANÓNICA del
|
|
24
|
+
* scope: él mismo TAL COMO ESTÁ EN TU TABLA (la fila leída, no lo que llegó
|
|
25
|
+
* en la pregunta) y después sus ancestros del más cercano a la raíz, con
|
|
26
|
+
* `APP_SCOPE` al final; y `null` si el scope NO existe (el motor deniega y
|
|
27
|
+
* rechaza escribir sobre él). Nunca devuelvas `[scope, APP_SCOPE]` para lo
|
|
28
|
+
* que no conoces: convertiría cualquier scope inventado en descendiente de la
|
|
29
|
+
* raíz. Devolver la fila (y no el argumento) es lo que impide que un alias del
|
|
30
|
+
* uuid —mayúsculas, guiones quitados, que el tipo `uuid` de PostgreSQL o una
|
|
31
|
+
* collation `*_ci` funden con la fila real— evada un deny escrito con la
|
|
32
|
+
* forma canónica. `hierarchicalScopeResolver({ nodeOf })` lo hace por ti a
|
|
33
|
+
* partir de un `nodeOf` que lea la fila.
|
|
23
34
|
*
|
|
24
|
-
* organization → [APP_SCOPE]
|
|
25
|
-
* unit → [
|
|
35
|
+
* organization → [la organization (fila), APP_SCOPE] (si existe; si no, null)
|
|
36
|
+
* unit → [la unit (fila), su organization (fila), APP_SCOPE] (idem)
|
|
37
|
+
*
|
|
38
|
+
* Esta MISMA función va a tres sitios, y tiene que ser la misma:
|
|
39
|
+
* - `scopes.resolveChain` (abajo): el manager valida con ella
|
|
40
|
+
* `scopes.attached/moved` (padre existente, sin ciclos) antes de tocar
|
|
41
|
+
* el driver;
|
|
42
|
+
* - `resolveChain` de cada driver: con ella responde `authorize`,
|
|
43
|
+
* `hasRole` y los `list*`, y valida `grant`/`deny`.
|
|
44
|
+
* Un driver sin ella solo conoce `app` (422 al preguntar por otro nivel).
|
|
45
|
+
*
|
|
46
|
+
* Cuando tu árbol cambie, avisa al motor en todos los drivers:
|
|
47
|
+
* authorization.scopes.attached(child, parent) / moved(child, newParent) /
|
|
48
|
+
* detached(child) ← purga los hechos del scope antes de que borres la fila.
|
|
26
49
|
*/
|
|
27
|
-
async function
|
|
28
|
-
if (scope.type === 'app') return []
|
|
29
|
-
|
|
50
|
+
async function resolveScopeChain(scope: ScopeRef): Promise<ScopeRef[] | null> {
|
|
51
|
+
if (scope.type === 'app') return [APP_SCOPE]
|
|
52
|
+
// TODO: consulta tu tabla; devuelve null si `scope.uuid` no existe, y si
|
|
53
|
+
// existe, [{ type, uuid: fila.uuid }, ...ancestros, APP_SCOPE].
|
|
54
|
+
return null
|
|
30
55
|
}
|
|
31
56
|
|
|
32
57
|
/**
|
|
@@ -46,15 +71,28 @@ export default defineConfig({
|
|
|
46
71
|
modelId: env.get('OPENFGA_MODEL_ID'),
|
|
47
72
|
},
|
|
48
73
|
|
|
74
|
+
// La costura del árbol (ver arriba): la misma función que reciben los drivers.
|
|
75
|
+
// 2.1: añade `descendantsOf: sqlDescendantsOf({ table, uuidColumn, parentColumn, typeColumn })`
|
|
76
|
+
// (2.2: con `descendantsOf`, `scopes.detached` purga también los roles
|
|
77
|
+
// locales del SUBÁRBOL —sin él, solo los del scope exacto— y el nivel de
|
|
78
|
+
// un rol local se comprueba contra el árbol de hoy; delegar hacia abajo
|
|
79
|
+
// —`lead@unit` con owner una org— funciona con o sin él)
|
|
80
|
+
// (o tu propia función) para `authorizedScopes`; y `requireWithin: true` / `requireActor: true`
|
|
81
|
+
// en cuanto todos los call-sites pasen `within` / `actor` (hasta entonces el manager avisa).
|
|
82
|
+
scopes: { resolveChain: resolveScopeChain },
|
|
83
|
+
|
|
49
84
|
drivers: {
|
|
50
|
-
database: () => new DatabaseAuthorizationDriver({
|
|
85
|
+
database: () => new DatabaseAuthorizationDriver({ resolveChain: resolveScopeChain }),
|
|
51
86
|
|
|
52
87
|
/**
|
|
53
88
|
* Backend externo opcional. Levanta el servidor y provisiona con
|
|
54
89
|
* `node ace openfga:provision`; migra los hechos con `openfga:import`.
|
|
90
|
+
* El driver vive en el subpath `/openfga` (lo único que importa el peer
|
|
91
|
+
* opcional `@openfga/sdk`) y se carga DENTRO de la factory: sin
|
|
92
|
+
* AUTHZ_DRIVER=openfga el SDK ni se instala ni se importa.
|
|
55
93
|
*/
|
|
56
94
|
openfga: async () => {
|
|
57
|
-
const { OpenFgaAuthorizationDriver } = await import('@jantstack/adonis-authz')
|
|
95
|
+
const { OpenFgaAuthorizationDriver } = await import('@jantstack/adonis-authz/openfga')
|
|
58
96
|
const apiUrl = env.get('OPENFGA_URL')
|
|
59
97
|
const storeId = env.get('OPENFGA_STORE_ID')
|
|
60
98
|
if (!apiUrl || !storeId) {
|
|
@@ -64,11 +102,27 @@ export default defineConfig({
|
|
|
64
102
|
apiUrl,
|
|
65
103
|
storeId,
|
|
66
104
|
modelId: env.get('OPENFGA_MODEL_ID'),
|
|
67
|
-
|
|
105
|
+
resolveChain: resolveScopeChain,
|
|
68
106
|
holderTypes,
|
|
69
107
|
})
|
|
70
108
|
},
|
|
71
109
|
},
|
|
72
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Catálogos de roles/permisos (uno por módulo). `node ace authz:catalog:sync`
|
|
113
|
+
* los sincroniza en orden (podando los vínculos que el config ya no lista);
|
|
114
|
+
* `node ace authz:catalog:diff` falla en CI si la base no coincide.
|
|
115
|
+
*/
|
|
116
|
+
catalogs: [async () => appAclCatalog()],
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Roles locales a un scope (2.2): `defineScopedRole(actor, ownerScope, spec)`
|
|
120
|
+
* solo puede meter en un rol local permisos de esta lista blanca (y que el
|
|
121
|
+
* actor tenga efectivos en el owner). Vacía = nadie delega nada. Los
|
|
122
|
+
* permisos de plataforma (`admin:*`, ajustes de organización) no deberían
|
|
123
|
+
* estar aquí.
|
|
124
|
+
*/
|
|
125
|
+
delegablePermissions: [],
|
|
126
|
+
|
|
73
127
|
hooks: { onWrite: onAuthzWrite },
|
|
74
128
|
})
|
|
@@ -14,31 +14,57 @@ import { BaseSchema } from '@adonisjs/lucid/schema'
|
|
|
14
14
|
* El catálogo (roles/permissions/role_permissions) es metadata compartida
|
|
15
15
|
* entre drivers; los HECHOS (assignments/denies) son lo único que otro
|
|
16
16
|
* driver (p.ej. openfga) materializaría fuera.
|
|
17
|
+
*
|
|
18
|
+
* Decisiones observadas en PostgreSQL y MySQL (2.5 · J3, la suite corre en
|
|
19
|
+
* los tres motores):
|
|
20
|
+
* - `holder_uuid`/`scope_uuid` son `varchar(64)`, NO `uuid`: la identidad
|
|
21
|
+
* es una cadena validada por la gramática del paquete (`[A-Za-z0-9._-]`,
|
|
22
|
+
* ≤ 36) y el uuid es del consumidor; el tipo `uuid` de PostgreSQL
|
|
23
|
+
* rechazaba cualquier id que no fuera un UUID.
|
|
24
|
+
* - Las columnas de identidad y los slugs llevan `collate 'utf8mb4_bin'`
|
|
25
|
+
* (solo MySQL lo compila; en PG y SQLite la comparación ya es binaria):
|
|
26
|
+
* con la collation por defecto (`*_ci`) `abc` y `ABC` eran la misma fila
|
|
27
|
+
* y un grant a uno autorizaba al otro.
|
|
28
|
+
* - `expires_at` es `DATETIME(3)` (`datetime` con precisión 3): el
|
|
29
|
+
* `timestamp` de MySQL redondea al segundo (una caducidad de +600 ms
|
|
30
|
+
* concedía hasta +1 s) y no admite fechas más allá de 2038.
|
|
17
31
|
*/
|
|
18
32
|
export default class extends BaseSchema {
|
|
19
33
|
public async up() {
|
|
20
34
|
this.schema.createTable('authz_roles', (table) => {
|
|
21
35
|
table.uuid('uuid').primary().notNullable()
|
|
22
|
-
table.string('slug', 100).notNullable()
|
|
36
|
+
table.string('slug', 100).notNullable().collate('utf8mb4_bin')
|
|
23
37
|
table.string('name', 100).notNullable()
|
|
24
38
|
table.string('description', 500).nullable()
|
|
25
39
|
// Nivel al que el rol es asignable: 'app' | 'organization' | 'unit'.
|
|
26
|
-
|
|
40
|
+
// Identidad: byte a byte, como el resto (2.5-B, auditor ⚪4).
|
|
41
|
+
table.string('scope_type', 20).notNullable().collate('utf8mb4_bin')
|
|
27
42
|
// Rango de privilegio (mayor = más poder). Lo consume la policy de
|
|
28
43
|
// asignación (role_assignment_policy): nadie otorga/quita por encima
|
|
29
44
|
// de su propio rango.
|
|
30
45
|
table.integer('rank').notNullable().defaultTo(0)
|
|
46
|
+
// Quién define el rol (3B · B1): `global` (el catálogo del config,
|
|
47
|
+
// `syncAuthzCatalog`) o la clave del scope que lo define
|
|
48
|
+
// (`<tipo>|<uuid>`, `defineScopedRole`). Un rol local solo existe
|
|
49
|
+
// dentro de su owner; dos tenants pueden definir `lead@unit` cada uno.
|
|
50
|
+
// Es identidad: byte a byte. `app` nunca es owner (sería global).
|
|
51
|
+
table.string('owner_scope_key', 80).notNullable().defaultTo('global').collate('utf8mb4_bin')
|
|
31
52
|
table.timestamp('created_at').notNullable()
|
|
32
53
|
table.timestamp('updated_at').notNullable()
|
|
33
54
|
|
|
34
|
-
table.unique(['slug', 'scope_type'], '
|
|
55
|
+
table.unique(['slug', 'scope_type', 'owner_scope_key'], 'authz_roles_slug_scope_owner_uq')
|
|
56
|
+
table.index(['owner_scope_key'], 'authz_roles_owner_idx')
|
|
35
57
|
})
|
|
36
58
|
|
|
37
59
|
this.schema.createTable('authz_permissions', (table) => {
|
|
38
60
|
table.uuid('uuid').primary().notNullable()
|
|
39
61
|
// Formato `recurso:accion` (la misma gramática que las abilities M3).
|
|
40
|
-
table.string('slug', 100).notNullable().unique()
|
|
62
|
+
table.string('slug', 100).notNullable().unique().collate('utf8mb4_bin')
|
|
41
63
|
table.string('description', 500).nullable()
|
|
64
|
+
// Niveles (scope types) cuyos roles PUEDEN llevar este permiso (3B ·
|
|
65
|
+
// B5): JSON `["app","organization"]`, o NULL = cualquiera. Control de
|
|
66
|
+
// COMPOSICIÓN (sync, defineScopedRole, grant); jamás de evaluación.
|
|
67
|
+
table.string('assignable_at', 500).nullable()
|
|
42
68
|
table.timestamp('created_at').notNullable()
|
|
43
69
|
table.timestamp('updated_at').notNullable()
|
|
44
70
|
})
|
|
@@ -65,22 +91,23 @@ export default class extends BaseSchema {
|
|
|
65
91
|
this.schema.createTable('authz_assignments', (table) => {
|
|
66
92
|
table.uuid('uuid').primary().notNullable()
|
|
67
93
|
// Holder polimórfico: morph name (users/admins/integrations/...).
|
|
68
|
-
table.string('holder_type', 50).notNullable()
|
|
69
|
-
table.
|
|
94
|
+
table.string('holder_type', 50).notNullable().collate('utf8mb4_bin')
|
|
95
|
+
table.string('holder_uuid', 64).notNullable().collate('utf8mb4_bin')
|
|
70
96
|
table
|
|
71
97
|
.uuid('role_uuid')
|
|
72
98
|
.notNullable()
|
|
73
99
|
.references('uuid')
|
|
74
100
|
.inTable('authz_roles')
|
|
75
101
|
.onDelete('RESTRICT')
|
|
76
|
-
table.string('scope_type', 20).notNullable()
|
|
102
|
+
table.string('scope_type', 20).notNullable().collate('utf8mb4_bin')
|
|
77
103
|
// Sin FK: el scope es polimórfico. El nivel 'app' usa el uuid CENTINELA
|
|
78
104
|
// 00000000-... (no NULL): los motores SQL tratan cada NULL como
|
|
79
105
|
// distinto y el unique no aplicaría — con centinela la idempotencia
|
|
80
106
|
// del grant la respalda la constraint en TODOS los niveles (sin
|
|
81
107
|
// features específicas de un motor, regla M6).
|
|
82
|
-
table.
|
|
83
|
-
|
|
108
|
+
table.string('scope_uuid', 64).notNullable().collate('utf8mb4_bin')
|
|
109
|
+
// Milisegundos y sin tope de 2038 (ver cabecera): DATETIME(3).
|
|
110
|
+
table.datetime('expires_at', { precision: 3 }).nullable()
|
|
84
111
|
table.timestamp('created_at').notNullable()
|
|
85
112
|
|
|
86
113
|
table.unique(
|
|
@@ -93,17 +120,17 @@ export default class extends BaseSchema {
|
|
|
93
120
|
|
|
94
121
|
this.schema.createTable('authz_denies', (table) => {
|
|
95
122
|
table.uuid('uuid').primary().notNullable()
|
|
96
|
-
table.string('holder_type', 50).notNullable()
|
|
97
|
-
table.
|
|
123
|
+
table.string('holder_type', 50).notNullable().collate('utf8mb4_bin')
|
|
124
|
+
table.string('holder_uuid', 64).notNullable().collate('utf8mb4_bin')
|
|
98
125
|
table
|
|
99
126
|
.uuid('permission_uuid')
|
|
100
127
|
.notNullable()
|
|
101
128
|
.references('uuid')
|
|
102
129
|
.inTable('authz_permissions')
|
|
103
130
|
.onDelete('CASCADE')
|
|
104
|
-
table.string('scope_type', 20).notNullable()
|
|
131
|
+
table.string('scope_type', 20).notNullable().collate('utf8mb4_bin')
|
|
105
132
|
// Centinela 00000000-... para 'app' (ver nota en authz_assignments).
|
|
106
|
-
table.
|
|
133
|
+
table.string('scope_uuid', 64).notNullable().collate('utf8mb4_bin')
|
|
107
134
|
table.timestamp('created_at').notNullable()
|
|
108
135
|
|
|
109
136
|
table.unique(
|
|
@@ -112,9 +139,26 @@ export default class extends BaseSchema {
|
|
|
112
139
|
)
|
|
113
140
|
table.index(['holder_type', 'holder_uuid'], 'authz_deny_holder_idx')
|
|
114
141
|
})
|
|
142
|
+
|
|
143
|
+
// Versión COMPARTIDA del catálogo (2.1): una fila (`id = 1`) cuyo entero
|
|
144
|
+
// sube `syncAuthzCatalog` dentro de su transacción. Los memos del
|
|
145
|
+
// catálogo de todos los procesos la contrastan antes de servir: un sync
|
|
146
|
+
// en un worker se ve en los demás sin reiniciar ni pub/sub. Quien
|
|
147
|
+
// escriba `authz_*` a mano lo hace con `withAuthzCatalogWrite(async (trx)
|
|
148
|
+
// => …)`: misma transacción, versión subida como última sentencia. Sin
|
|
149
|
+
// esta fila (id = 1) toda pregunta es 503: por eso se siembra aquí.
|
|
150
|
+
this.schema.createTable('authz_catalog_version', (table) => {
|
|
151
|
+
table.integer('id').primary().notNullable()
|
|
152
|
+
table.bigInteger('version').notNullable().defaultTo(0)
|
|
153
|
+
table.timestamp('updated_at').notNullable()
|
|
154
|
+
})
|
|
155
|
+
this.defer(async (db) => {
|
|
156
|
+
await db.table('authz_catalog_version').insert({ id: 1, version: 0, updated_at: new Date() })
|
|
157
|
+
})
|
|
115
158
|
}
|
|
116
159
|
|
|
117
160
|
public async down() {
|
|
161
|
+
this.schema.dropTable('authz_catalog_version')
|
|
118
162
|
this.schema.dropTable('authz_denies')
|
|
119
163
|
this.schema.dropTable('authz_assignments')
|
|
120
164
|
this.schema.dropTable('authz_role_permissions')
|
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jantstack/adonis-authz",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-alpha.1",
|
|
4
4
|
"description": "Driver-based authorization engine for AdonisJS + Lucid: hierarchical scopes with downward inheritance, explicit denies, expiring assignments and polymorphic holders. Ships a self-contained database driver and an OpenFGA driver, both judged by the same executable contract suite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"author": "Jose Tenorio",
|
|
8
7
|
"repository": {
|
|
9
8
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/jantstack/adonis-authz.git"
|
|
11
10
|
},
|
|
12
|
-
"homepage": "https://github.com/
|
|
11
|
+
"homepage": "https://github.com/jantstack/adonis-authz#readme",
|
|
13
12
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/
|
|
13
|
+
"url": "https://github.com/jantstack/adonis-authz/issues"
|
|
15
14
|
},
|
|
16
15
|
"keywords": [
|
|
17
16
|
"adonisjs",
|
|
@@ -43,6 +42,7 @@
|
|
|
43
42
|
"types": "./build/index.d.ts",
|
|
44
43
|
"exports": {
|
|
45
44
|
".": "./build/index.js",
|
|
45
|
+
"./openfga": "./build/src/openfga.js",
|
|
46
46
|
"./authz_provider": "./build/providers/authz_provider.js",
|
|
47
47
|
"./services/main": "./build/services/main.js",
|
|
48
48
|
"./app_access_middleware": "./build/src/middleware/app_access_middleware.js",
|
|
@@ -61,7 +61,10 @@
|
|
|
61
61
|
"prepublishOnly": "npm run build",
|
|
62
62
|
"copy:stubs": "mkdir -p build/stubs/config && cp stubs/*.stub build/stubs/ && cp stubs/config/*.stub build/stubs/config/",
|
|
63
63
|
"check:purity": "node scripts/check_purity.mjs",
|
|
64
|
-
"test": "node --import @poppinss/ts-exec bin/test.ts",
|
|
64
|
+
"test": "npm run check:purity && node --import @poppinss/ts-exec bin/test.ts",
|
|
65
|
+
"test:sqlite-file": "TEST_DB=sqlite-file npm test",
|
|
66
|
+
"test:pg": "TEST_DB=pg npm test",
|
|
67
|
+
"test:mysql": "TEST_DB=mysql npm test",
|
|
65
68
|
"typecheck:build": "tsc --noEmit"
|
|
66
69
|
},
|
|
67
70
|
"peerDependencies": {
|
|
@@ -87,6 +90,8 @@
|
|
|
87
90
|
"@types/node": "^22.19.1",
|
|
88
91
|
"better-sqlite3": "^13.0.1",
|
|
89
92
|
"luxon": "^3.7.2",
|
|
93
|
+
"mysql2": "^3.24.2",
|
|
94
|
+
"pg": "^8.23.0",
|
|
90
95
|
"typescript": "~5.9.3"
|
|
91
96
|
},
|
|
92
97
|
"publishConfig": {
|