Published

🔑 PIEZA · GRANTS — la retícula de privilegios

Connect any source, model it as an ontology, transform it, and operationalize it, analytics, automation and machine learning, under one governed, self-hostable roof. --- Most teams stitch the...

🔑 PIEZA · GRANTS — la retícula de privilegios

Versiónv1.0
Estado🏁 En producción y APLICANDO (USER_AUTHZ=enforce)
Última medición2026-08-12g0 · g1 · g2 · g3, todos verdes
Manda sobre ellaeste documento. El plan vive en plano-gobernado-approach.md §7·octies

1 · Qué es

La respuesta a una sola pregunta: «¿puede este sujeto ejercer este privilegio
sobre este objeto?»
— y su porqué, para poder auditarla.

⛔ Y qué NO es, que ahorra más que la definición

No esEs
No es autenticaciónLa identidad viene de fuera (Clerk / Keycloak). Aquí sólo se referencia
No es grano finoDecide si tocas la tabla, nunca qué ves dentro. Eso es ROW-POLICY, y no existe
No es una lista de prohibicionesDML_VERBS_PERMITIDOS es un sucedáneo que existía porque no había a quién conceder
No es una tabla de usuariosNo hay tabla principals: un sujeto es un par (kind, id) de otro sistema
No tiene denyEs aditivo. Un forbid explícito es de Cedar/OpenFGA — meterlo aquí serían dos motores de política

2 · Su naturaleza

   IDENTIDAD                  LA CADENA (Index)                      EL OBJETO
   ─────────                  ─────────────────                      ─────────
   Clerk    ──┐
   Keycloak ──┼─▶ principal ──▶ principal role ──▶ catalog role ──▶ grant ──▶ securable
   (agentes)──┘   (kind,id)      «el equipo»       «el paquete»    (privilegio)   │
                                                          workspace › catalog › schema › dataset
                                                                    (la HERENCIA baja por aquí)

Las dos relaciones son N:M, y el nivel de en medio es lo que resuelve «este equipo lee el esquema entero salvo dos tablas». Con un solo nivel habría que repetir el paquete de permisos por cada equipo.

Las seis propiedades de su autoridad

PropiedadSu consecuencia, entera
Aditivo, sin denyNo se puede expresar «todos menos éste»
Hereda hacia abajo por la cadena⭐ Es lo que hace que los future grants de Snowflake no hagan falta: lo que nazca en un contenedor concedido nace cubierto
Implica transitivamenteCATALOG_MANAGE_CONTENT arrastra 10 más. Se calcula con visitados: un ciclo sería un error de datos, pero un bucle en el camino de autorización sería una caída
Gana el más específicoNo por precedencia —el modelo es aditivo— sino para explicar: el nivel más cercano al objeto es el que un humano espera ver
Devuelve el PORQUÉSin grantedBy, una autorización no se puede auditar
Grano grueso, y sólo esoVer §1

El vocabulario: 40 privilegios en 6 familias

WORKSPACE_* (4) · CATALOG_* (7) · SCHEMA_* (8) · TABLE_* (10) · VIEW_* (6) · POLICY_* (6)

El invariante que más se equivoca al copiarlo: *_FULL_METADATA no abre el dato. Leer la metadata de una tabla y leer sus filas son permisos distintos (TABLE_READ_DATA va aparte, deliberadamente), y fundirlos es cómo se filtran datos por accidente.


3 · Dónde vive

RutaQué es
Vocabulario y decisiónlib/governance/privilege.tsMódulo puro: los 40 privilegios, grantableOn, implies, decidePrivilege. Sin I/O
La I/Olib/governance/privilege-queries.tsCarga los grants. Lanza si falla — un error de lectura no puede confundirse con «sin permisos»
El esquemasupabase/migrations/20261269_privilege_chain.sql5 tablas + 3 triggers de tenencia + la vista principal_grants
Plantilla de SERVICIOSlib/governance/tenant-template.ts7 catalog roles · 7 principal roles. Un inquilino nace con ellos
Derivación de HUMANOSlib/governance/membership-grants.ts + -queries.tsLa proyección de workspace_members
Aplicador de humanoslib/governance/user-authz.tsoff / shadow / enforce
Aplicador de motoreslib/governance/catalog-authz.tsLa cara Iceberg REST

⚠️ Por qué el vocabulario vive en TypeScript y no en SQL: el CHECK de la migración es sólo un patrón. Los 40 nombres, dónde se concede cada uno y qué implica viven en un solo sitio — repetirlos en SQL sería el mismo conocimiento en dos lenguajes, que es la clase de bug que ya costó una fase.


4 · Quién la ejerce — dos puertas, dos sujetos distintos

   ┌─ LA PUERTA (Junction) ────────────────────────────────────────┐
   │  pregunta: «¿puede esta PERSONA?»                             │
   │  lib/compute/junction.ts → user-authz.ts                      │
   │  en lectura, escritura y CREATE · interruptor USER_AUTHZ      │
   └───────────────────────────────────────────────────────────────┘
   ┌─ LA CARA (/api/iceberg) ──────────────────────────────────────┐
   │  pregunta: «¿puede este MOTOR?»                               │
   │  lib/governance/catalog-authz.ts · fail-closed · deja asiento │
   └───────────────────────────────────────────────────────────────┘

La decisión más importante de la cara: loadTable sin delegación pide TABLE_READ_PROPERTIES; con X-Iceberg-Access-Delegation pide además TABLE_READ_DATA. Porque son dos cosas: la primera dice dónde está la tabla; la segunda entrega la llave para leer sus bytes.

⚠️ Y lo que ninguna de las dos puertas hace: cerrar la red. Si un motor puede apuntar al catálogo directamente, esto no aplica nada. Es infraestructura, no código — y la parte más barata y más olvidable del trabajo.


5 · Lo medido (2026-08-12)

Los sujetos

principal_grants ....... 368 filas de SERVICIO  (8 sujetos, 8 inquilinos)
                   +  12 sujetos HUMANOS, derivados        ← F·2·0
privilege_grants ....... 248 · principal_roles 96 · catalog_roles 56 · bindings 120
Rol de membresíaRol derivadoPrestaSujetos hoy
ownerws-owneroperator3
adminws-adminwriter6
editorws-editorreader + dml-writer0
viewerws-viewerreader3
guestws-guestcatalog-browser0

⭐⭐ owner y admin NO reciben lo mismo, y ahí está la separación de deberes: sólo owner lleva WORKSPACE_MANAGE_ACCESS. Conceder es un oficio distinto de crear, y Clerk ya distinguía los dos nombres — implementarlo no costó diseño, sólo verlo.

Los gates

npx dotenv -e .env.local -- npx tsx scripts/governance/g0-censo-de-sujetos.ts
npx dotenv -e .env.local -- npx tsx scripts/governance/g1-derivar-grants-de-usuario.ts --apply  # 24/24
npx dotenv -e .env.local -- npx tsx scripts/governance/g2-aplicador-de-usuario.ts               # 22/22
npx dotenv -e .env.local -- npx tsx scripts/governance/g3-future-grants.ts                      #  5/5
npx vitest run lib/governance/membership-grants.test.ts                                         # 20/20
curl -H "x-diag-key: $SPARK_BRIDGE_JWT_SECRET" https://app.paladio.io/api/diag/motor            # userAuthz

Lo que cada uno prueba, que no es lo mismo que «pasa»:

GateQué demuestra
g1La derivación decide distinto por rol, con control negativo (un ajeno recibe denegación de verdad)
g2El aplicador decide · y el pre-vuelo pregunta por CADA sujeto, no por una muestra: 5/5 conservan su capacidad
g3Los future grants no hacen falta: 123/123 cadenas nacen en su workspace y 168 grants, ninguno sobre un dataset suelto
testsLa proyección retira además de conceder: degradar quita el rol viejo

6 · El estándar

FuenteQué se TOMAQué NO
Unity CatalogLa retícula sobre securables con herencia, y que la propiedad da todo sobre ese objetoSu ABAC: no es OSS, es del producto gestionado
Apache PolarisEl vocabulario de privilegios — está calcadoSu grano fino: no tiene
Snowflake«No ata privilegios a usuarios: los roles llevan los grants» · propiedad de rol · SECURITYADMINSYSADMINGRANT … ON FUTURE: su modelo lo necesita, el nuestro no (②)
UC (operativo)«No modifiques los grupos aquí: usa tu IdP»

⭐ Las dos divergencias deliberadas, dichas enteras

  1. Un grant de workspace alcanza al DATO. En UC los grants de metastore no se heredan al dato. Pero nuestro workspace juega el papel del CATÁLOGO de UC —que sí hereda—, no el del metastore. La divergencia es de nombre, no de modelo. ⚠️ El cabo real: WORKSPACE_MANAGE_ACCESS (la retícula) y WORKSPACE_MANAGE_CONTENT (el dato) conviven en un nivel donde uno debe heredar y el otro no.
  2. Los humanos se DERIVAN y los servicios se DECLARAN. No es incoherencia: un servicio no está escrito en ninguna otra parte; una persona sí. La regla no es «derivar siempre», es «derivar cuando hay de qué».

7 · Lo que falta — con su gate

QuéGate
El canary del camino HTTP enteroUna petición real de un viewer a /api/sql-editor/execute que reciba 403. g2 mide la función que la puerta llama, no la petición
🟡La propiedad como ejeAplazada con motivo medido: ⛔ datasets.created_by no es «el dueño», es «quién lo tecleó» — 14 datasets lo tienen apuntando a un SERVICIO. Entra cuando haya una columna que signifique dueño y sea de un rol
🟡Estrechar DML_VERBS_PERMITIDOS por objetoYa hay a quién conceder. La lista no se retira borrando locateDmlCommand: se retira cuando el permiso decide por objeto
🟡agent y job como sujetosEl vocabulario ya los admite y no hay ni uno. Falta el patrocinador humano y el propósito acotado (Principal.purpose existe y no se usa)
🟡La superficie del clienteQue un cliente administre sus roles sin que intervenga nadie. WORKSPACE_MANAGE_ACCESS sólo lo tiene ws-owner
Grano finoNo es de esta pieza: es ROW-POLICY

8 · Historial

VersiónFechaQué cambió de su NATURALEZA
v1.02026-08-12Deja de ser inerte. Entran los sujetos humanos (derivados de la membresía), el aplicador de personas, y USER_AUTHZ=enforce. Y se cierra que los future grants no hacen falta
v0.32026-08-05La plantilla de inquilino: un workspace nace con sus roles de servicio
v0.22026-08La cara Iceberg REST empieza a aplicar (C5/N·3a) — deja de ser decoración
v0.1C3: la cadena de roles, calcada de Polaris. Nace inerte a propósito: «C3 sin C5 es decoración»