Published

El plano de datos gobernado — approach VIVO

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...

El plano de datos gobernado — approach VIVO

Documento de trabajo. Se le van haciendo incisos sesión a sesión; cuando esté
maduro se funde en un sustrato-07 limpio. Sustituye y absorbe a
politicas-de-celda-approach.md.

Regla de la casa: cada hecho lleva el comando que lo demuestra. Lo que no lo lleva
va marcado como pendiente (⏳) u opinión.

Abierto: 2026-08-12. Manda sustrato-06.md donde discrepen —
hasta que nazca el 07.


⭐⭐⭐ El encuadre completo vive en
index-gobernanza-mapa.md
— el mapa de posición
concepto a concepto frente al estándar, los cinco planos de autoridad, qué
grants proponemos y de dónde sale su autoridad, y dónde aplica cada jugador.
Este documento es el PLAN; aquél es la DEFINICIÓN DE HECHO.


1 · El norte, en dos frases

Los bytes no saben de nadie. El catálogo lo sabe todo y es donde se APLICA. Todo lo
demás son proyecciones que leen por la misma puerta, y por eso heredan la política sin
reimplementarla.

Y la ontología no es un grafo con permisos encima: ES el paradigma de tipos,
permisos, validaciones y políticas.
El grafo es cómo se representa. Un agente no
consume un espacio de exploración: consume un espacio de acción acotado.

   ┌── PROYECCIONES ─────────────────────────────────────────────┐
   │  SQL  ·  la ONTOLOGÍA (grafo)  ·  los agentes  ·  la API     │
   │  ninguna copia el dato · todas entran por la misma puerta    │
   └───────────────────────────┬─────────────────────────────────┘
        ⭐ LA CINTURA GOBERNADA — el catálogo
        nombres · tipos · ETIQUETAS · POLÍTICAS · linaje · contrato
        y **el punto donde se aplica**:  planTableScan
                     bytes: Iceberg + R2
                     no saben de nadie, y está bien así

2 · ⭐⭐⭐ EL PRINCIPIO RECTOR — se toma prestado el MODELO, se deriva en NUESTRO sustrato

Es la clave del éxito y merece estar antes que nada, porque explica a la vez qué hacemos y qué error no repetimos.

2·1 · La distinción, en una frase

Un tipo que no deriva del catálogo es una DECLARACIÓN. Un tipo derivado del catálogo
es un CONTRATO.

El plano ontológico legacy (object_types/link_types/action_types) copió la forma de Foundry sin tener plano de datos debajo. El resultado tiene el vocabulario correcto y no gobierna nada, porque nada deriva de nada. La lección no es «no copiar»: es que copiar el modelo es barato y correcto; copiar la implementación sin su sustrato es lo que produce un plano muerto.

2·2 · Las tres capas de todo lo que adoptamos

Para cada tecnología de referencia, siempre las mismas tres preguntas:

PreguntaEjemplo
① El MODELO¿Qué conceptos y vocabulario tiene?UC: privilegios sobre securables con herencia
② La DERIVACIÓN¿De qué dato NUESTRO sale cada concepto?el securable es una fila de datasets/schemas de Index
③ El PRODUCTO¿Instalamos su implementación?casi siempre NO

①se estudia · ② se construye · ③ se evita salvo prueba en contrario. Un modelo adoptado sin ② es exactamente el legacy que vamos a retirar.

2·3 · La tabla de derivación — qué tomamos de cada uno y de qué sale

Fuente① Qué se TOMA (modelo)⛔ Qué NO se toma② De qué DERIVA en nuestro sustrato
Unity Catalog OSSLa retícula de privilegios (SELECT/MODIFY/USE SCHEMA…) sobre securables con herencia por contenedor · y ABAC por etiquetas: la política se escribe sobre clasificaciones, no sobre objetos uno a unoSu metastore, su runtime, su modelo de identidadEl securable ya existe: principal_grants(securable_kind, securable_id). La herencia ya existe: decidePrivilege resuelve por cadena. Falta el eje de etiquetas y los grants de usuario
FoundryLa forma de Action: operación de escritura tipada, validada, con permisos y auditada, sobre un tipo de objetoSu implementación (es lo que copió el legacy) · su idea de que el grafo es el productoLa Action es un verbo gobernado de los nuestros con parámetros y precondiciones: escribe por la puerta, hereda la política de celda y deja asiento en el ledger que ya tenemos
Apache Iceberg (spec REST)planTableScan: el catálogo planifica y devuelve sólo lo autorizado ⇒ un punto de aplicación para N motores— (aquí sí adoptamos el estándar tal cual)Nuestra cara (/api/iceberg/v1) ya es un Iceberg REST gobernado: es donde entra planTableScan
ODCS / BitolEl contrato de activo: propiedades con semántica de negocio y atributos de gobernanza (esto es PII, sólo estos roles)Su tooling de vendorsEl contrato nace en la ingesta y es lo que define el tipo. Se guarda en Index junto al dataset, no en un servicio aparte
Ontop (patrón VKG)La proyección virtual: SPARQL reescrito a SQL, sin copiar el datoEl producto, salvo que la reescritura salga cara de hacerEl grafo se proyecta desde el catálogo y lee por la puerta ⇒ hereda política sin implementarla
OpenFGAReBAC: relaciones en vez de filas (equipos, propiedad, compartición, y gobernar el traversing)Ser una segunda fuente de políticaEs el decisor de la retícula que ya existe, no una retícula nueva
Apache EgeriaVocabulario de gobernanza: governance actions, clasificaciones, zonasEl runtime (cohortes Kafka) — desproporcionadoSe saquea el diccionario, se implementa sobre Index
Apache AtlasSu sistema de tipos (entidades, relaciones, clasificaciones, atributos), que fue pioneroTodo el producto: HBase + Solr + Kafka, era HadoopSe lee como referencia histórica del modelo de tipos, nada más
Spark (error conditions)Condición estable + SQLSTATE + parámetros, y la regla: no manejes por el texto— (se reexporta tal cual, ver errores-gobernados-approach.md)Nuestros rechazos pasan a tener condición y SQLSTATE propios en la clase KD
Apache RangerEl patrón: autorizar desde el plan analizado, con objetos ya resueltosEl producto: extensión JVM + su propio almacén de políticasYa adoptado: es lo que hace plan-reader + la puerta

2·4 · La prueba del algodón, para cada pieza nueva

Antes de adoptar nada, tres preguntas. Si la segunda no tiene respuesta, no se adopta:

  1. ¿Qué problema nuestro resuelve, medido? (no «qué hace bien»)
  2. ¿De qué dato NUESTRO deriva? Si la respuesta es «se declara», es el legacy otra vez con otro nombre.
  3. ¿Podemos quedarnos con el modelo y no con el producto? Si la respuesta es no, hay que justificar el servicio nuevo con su coste de operación.

3 · Dónde estamos — medido

3·1 · Lo que HAY y funciona

Evidencia
Un punto de aplicación por objeto: la cara autoriza por inquilino y tabla en cada peticiónCATALOG_OPS · decidePrivilege (herencia por cadena)
Una retícula con forma de securable, pobladaprincipal_grants: 368 filas
El plan del motor con objetos resueltos, privilegio y rol destino/fuenteP·0/P·1 · plan-reader
Verbos gobernados con destino resuelto por el plan, y reconciliación de IndexALTER TABLE … ADD COLUMN en producción (2026-08-12)
Iceberg 1.11.0 en el cluster ⇒ el cliente de scan planning está puestoiceberg-spark-runtime-4.1_2.13-1.11.0.jar
Coordenada estricta y namespace opacosustrato-06 §7·ter · N·5

3·2 · Lo que NO hay

Evidencia
Roles y políticas definidos por el CLIENTEno existe superficie
Nivel de celda (fila/columna)Iceberg no lo da por diseño (§4)
Grants de usuario368 de 368 son de servicios
Etiquetas sobre las que escribir políticano existen
Política desde la ingestala ingesta escribe sin contrato ni etiquetas
Un solo modelo de tipos y permisoshay dos planos (§3·3)

3·3 · ⚠️ El plano ontológico legacy — censo y veredicto

object_types             17        action_types               4
link_types                4        action_type_executions     4   ⚠️ CUATRO
object_type_properties  247        objects               27.042

Tiene la forma correctatarget_object_type_id, can_execute_action_type, contrato, versiones, trazas, denial_context— y dos defectos que lo invalidan:

  1. executeObjectActionType / syncOutputToOntology escriben en PG sin pasar por la puerta del Warehouse.
  2. can_execute_action_type es su propio modelo de permisos (role + submission), distinto de principal_grants/CATALOG_OPS.

⇒ Una copia materializada con su propio motor de política: dos motores divergen siempre, y cuando diverjan el grafo enseñará lo que la tabla ocultaba.

🏁 DECISIÓN (owner, 2026-08-12): se rehace. Y es barato — 4 ejecuciones en toda su historia: es arquitectura sin tráfico. 17 tipos y 247 propiedades se rehacen a mano. Los 27.042 objetos son el residuo, no el obstáculo: bajo el norte no deberían existir como copia.


4 · La pregunta de Iceberg, contestada

La fricción NO es con Iceberg, y migrar de formato no quitaría nada.

Iceberg no aplica seguridad de fila ni de columna, y es deliberado: la seguridad pertenece al catálogo y al motor, no al formato. Lo mismo vale para Delta y Hudi.

La fricción real es dónde aplicamos:

⛔⛔ Hoy vendamos credencial STS (900 s) para que el motor lea el Parquet directo.
Mientras eso siga así, cualquier política de fila es DECORATIVA.

⭐ Y el mecanismo que lo resuelve ya está estandarizado

Iceberg 1.11 metió el scan planning en el SERVIDOR: el motor manda POST …/plan, el catálogo aplica los filtros de fila y máscaras de columna que la política permita a ESE llamante, y devuelve un plan que apunta sólo a datos autorizados. Una vez, en el catálogo, para todos los motores. Es lo que Unity Catalog expone como ABAC cross-engine.

Ya corremos Iceberg 1.11.0. El lado cliente está puesto; falta el servidor → F·0.


5 · Lo que queda parcialmente OBSOLETO de nuestro paradigma

QuéPor qué
«No dependeremos de server-side scan planning» (07-08)Se decidió porque Karma planificaba en cliente. Hoy es el mecanismo de la industria para política entre motores. Reabrir
⛔⛔El vendado de credencial directoIncompatible con política de fila. No hay término medio → F·1
🟡«El cómputo es plural y desechable»Sólo se sostiene si la política se aplica en el catálogo; en el motor, cada motor nuevo obliga a reimplementarla
🟡La retícula por securable uno a unoEl modelo maduro concede sobre etiquetas. La tabla no se tira: se le añade el eje
🟡Karma con índice y planificación propiosQuien planifica en cliente se salta la política
🟡OpenMetadata como fuente de verdadEl modelo de etiquetas debe vivir en Index (es lo que lee el motor de políticas). Se degrada a productor de etiquetas iniciales, y sólo si su detección ahorra trabajo medible

Y lo que se REFUERZA: la cintura estrecha en el catálogo · el trabajo de verbos gobernados (es el sustrato de las Actions, no un desvío) · el plan del motor como contrato.


6 · Los conceptos que hay que dominar

ConceptoPor qué nos afecta
ABAC vs RBAC vs ReBACLa política madura se escribe sobre etiquetas, no sobre objetos. Es lo que hace que N tablas nuevas no cuesten N políticas
Etiquetas / clasificacionesEl sustantivo de la política. Nacen en la ingesta
Row filter y column mask como objetos de primera claseVersionados, con dueño y auditoría. La diferencia entre una política y una costumbre
Server-side scan planningEl punto donde la política deja de depender del motor
Credential vending vs enforcementLa tensión de §4
Policy as code (Cedar / OPA-Rego)Ya hay decisión previa por Cedar, sin activar
Data contracts (ODCS)Donde viven definiciones, validaciones y guardarraíles — y lo que define el tipo
Purpose-based accessPrincipal.purpose existe y no se usa
SHACLValidación de formas del grafo (lo que se proyecta), frente a ODCS (lo que entra)

7 · El plan

QuéGateEstado
🏁 F·0¿Implementa Lakekeeper planTableScan?NO (§7·bis)scripts/warehouse/f0-scan-planning.tsHECHO 2026-08-12
🏁 F·1El vendado de credencial cuando hay política → lo contesta el estándar, no nosotros (§7·ter)la práctica de UC/Polaris/Dremio/SnowflakeHECHO 2026-08-12
🏁 F·2·0LOS GRANTS DE USUARIO — la pieza que falta en cinco sitios (§7·sexies), con el paradigma maduro del §7·septies (§7·octies)g0-censo-de-sujetos.ts · g1-derivar-grants-de-usuario.ts --apply24/24 · g2-aplicador-de-usuario.ts17/17 · g3-future-grants.ts5/5 · 20 tests purosHECHO 2026-08-12 · queda encender (USER_AUTHZ=enforce)
F·2El modelo de políticas con el vocabulario de UC: tag, row_filter, column_mask, role como objetos de Indexesquema + tests🟡 EN CURSO (08-14) · row_filter y column_mask declarados + aplicador puro construido (20 tests) + migración escrita sin aplicar. Falta el reescritor que use su salida, y tag como objeto. Ver §7·nonies
F·3Las etiquetas nacen en la ingesta (contrato ODCS)un dataset ingerido llega con sus etiquetas
F·4El aplicador: filtro y máscara donde se decidió en F·0/F·1un usuario ve menos filas y no puede esquivarlo · con control negativo
F·5La superficie del cliente: sus roles y sus políticasun cliente crea rol y política sin que intervenga nadie
F·6Unificar el plano de Actions con el del Warehouse: un modelo de tipos, una retícula, escritura por la puertauna Action hereda la política de celda sin implementarla
F·7El grafo, como proyección

F·0 y F·1 eran PREGUNTAS, no implementaciones — las dos contestadas el 08-12.

⭐⭐ Y F·2·0 va por delante de F·2 Y de P·4, y conviene decir por qué de las dos formas, porque son distintas:

  • por delante de F·2 — una política sin sujetos no se puede probar;
  • por delante de P·4 — la lista de verbos no se retira borrando locateDmlCommand: se retira cuando hay a quién conceder. Mientras la autorización sea global, cada verbo seguirá siendo una decisión de plataforma en vez de una concesión de permiso (escrituras-sustrato-debilidades.md §3). No se rehace el plano ontológico hasta contestarlas: si la política de celda no se puede aplicar en el catálogo, una Action no puede heredarla, y estaríamos construyendo el mismo error otra vez con mejor vocabulario.

7·bis · 🏁 F·0 — CONTESTADA: Lakekeeper NO planifica en el servidor

Medido el 2026-08-12 (scripts/warehouse/f0-scan-planning.ts). El catálogo publica sus endpoints soportados —como manda la spec REST desde 1.6— y son 25, sin ninguno de planificación:

GET  /v1/config                                       POST /v1/{p}/namespaces/{ns}/tables
GET  /v1/{p}/namespaces                               GET  …/tables/{table}
HEAD/POST/GET/DELETE  …/namespaces/{ns}                POST/DELETE/HEAD …/tables/{table}
POST …/namespaces/{ns}/properties                     ⭐ GET  …/tables/{table}/credentials
GET  …/namespaces/{ns}/tables                          POST …/tables/{table}/metrics
POST /v1/{p}/tables/rename                             POST /v1/{p}/transactions/commit
POST …/namespaces/{ns}/register                        + 6 endpoints de vistas

⛔ NO hay:  POST …/tables/{table}/plan  ·  …/plan-tasks  ·  planTableScan

⚠️ Reserva honesta sobre el instrumento: la sonda hace además un POST …/plan real, y devolvió 404 — pero la tabla de prueba también da 404, así que ese 404 no distingue «ruta inexistente» de «tabla no encontrada». El veredicto se apoya en la declaración de endpoints[], que es autoritativa, no en el POST.

Lo que esto decide

  1. La política de celda NO se puede aplicar hoy en nuestro catálogo. El mecanismo que la industria eligió no está disponible en la pieza que tenemos.
  2. Y GET …/tables/{table}/credentials SÍ está declarado: el vendado de credencial es un endpoint de primera clase de nuestro catálogo. ⇒ F·1 deja de ser una decisión de diseño y pasa a ser LA decisión, porque hoy hay una puerta abierta y ninguna forma de filtrar detrás de ella.

Las tres salidas, sin maquillar

QuéCosteRiesgo
A · Aplicar en el MOTORInyectar filtro y máscara en el plan, en la puerta (donde ya sabemos qué tablas toca la query)Lo más barato AHORA: reusa todo lo de P·0-P·5Impuesto por motor: cada motor nuevo obliga a reimplementarlo. Y hay que cerrar el vendado, o se esquiva leyendo el fichero
B · Contribuir el scan planning a LakekeeperImplementar planTableScan en un catálogo Rust ajenoAlto y con calendario ajenoEs el sitio correcto, pero no controlamos su hoja de ruta
C · Cambiar de catálogoA uno que lo implementeMigración del control-plane enteroNinguno lo tiene maduro todavía ⛔ CADUCADO 08-12: Gravitino 1.2.0 YA implementa scan planning offload ⛔⛔ DESMENTIDO EN VIVO 08-14 — ver §7·bis·2

7·bis·3 · ⭐⭐⭐ F·0·c — EL SCAN PLANNING FUNCIONA (y §7·bis·2 estaba mal)

Medido el 2026-08-14 (scripts/warehouse/f0c-scan-endpoint-real.ts):

POST …/tables/{tabla}/scan   → 200 · status=completed · plan-tasks ✅
POST …/tables/{tabla}/plan   → 500 NotFoundException

🪤 El servidor ANUNCIA una ruta y SIRVE otra. Su /v1/config publica POST /v1/{prefix}/tables/{table}/plan (la constante V1_SUBMIT_TABLE_SCAN_PLAN de la spec), pero el recurso registrado en IcebergTableOperations es {table}/scan — comprobado abriendo el jar. §7·bis·2 concluyó «no lo sirve» tras probar sólo /plan, y era falso.

⇒ La lección se invierte: endpoints[] no sólo promete de más; también puede nombrar mal lo que sí existe. Cuando la ruta anunciada falle, buscar la ruta real en el binario antes de concluir ausencia.

El bloqueante de la política de celda CAE, y la salida C se reabre

La política de fila/columna puede aplicarse en el catálogo, que es donde deja de depender del motor — y es exactamente lo que hace el estándar en 2026: UC y Snowflake Horizon aplican máscaras y filtros durante el server-side scan planning, de modo que la política vale para cualquier motor con cliente Iceberg 1.11. Nuestro Spark ya trae 1.11.0.

Lo que falta para usarlo, medido:

  1. La cara no enruta /scan (routeCatalogRequest no lo conoce).
  2. La cara anuncia endpoints: [] y no publica scan-planning-mode, así que ningún motor sabe que puede delegar la planificación.
  3. El PlanTableScanRequest lleva filter y select: ahí es donde la cara inyectaría el predicado del aplicador y recortaría columnas.
  4. ⚠️ Gravitino no conoce nuestras políticas (viven en Index). A diferencia de UC, aquí el catálogo planifica pero la política la pone la cara al reenviar — lo que mantiene a Index como fuente única y no parte el modelo en dos.

7·bis·2 · ⛔ F·0·b — el catálogo NUEVO lo ANUNCIA y no lo sirve CORREGIDO por §7·bis·3

Medido el 2026-08-14 (scripts/warehouse/f0b-scan-planning-gravitino.ts), ya con Gravitino 1.2.0 en producción y el cutover cerrado.

El /v1/config del catálogo nuevo sí declara el endpoint que a Lakekeeper le faltaba:

POST /v1/{prefix}/tables/{table}/plan        ← anunciado entre sus 24 endpoints

Pero no responde. Por las DOS rutas —la anunciada y la de la spec, con namespace—:

javax.ws.rs.NotFoundException: HTTP 404 Not Found   (envuelto en un 500)

Es un 404 de Jersey: el recurso no está registrado en el servidor. No es un 4xx de forma del cuerpo, que sería «existe y no te entiendo». Y no es un flag apagado: no hay ninguna clave de plan/scan en gravitino.conf.

⚠️ Reserva honesta: no se ha probado el IRC suelto ni otra build. Lo medido es el modo auxiliar plegado, que es el que está en producción (D·5) — y ya se sabe que ese modo no trae los mismos jars que el suelto (§4 del handoff del 08-14).

⭐⭐⭐ Lo que esto enseña, y vale más que el dato

La lista endpoints[] de /v1/config es una DECLARACIÓN, no un inventario de lo que
el servidor sirve.

F·0 apoyó su veredicto en esa lista precisamente por considerarla autoritativa («el veredicto se apoya en la declaración de endpoints[], no en el POST»). Contra Lakekeeper acertó por casualidad: allí la lista y la realidad coincidían. Aquí divergen, y un cliente Iceberg que lea la declaración y decida delegar la planificación se comerá un 404.

Es la regla 90 en su forma más cara: la fuente que copias puede no haber tenido nunca el valor bueno — y la nota que declaraba caducada la salida C venía de documentación de terceros, no de una medición nuestra.

Lo que NO cambia (y desbloquea trabajo igualmente)

F·2 no depende de esto. Su dependencia declarada es F·2·0 —los grants de usuario—, que está CERRADA (USER_AUTHZ=enforce en producción desde el 08-12). El modelo de políticas (tag, row_filter, column_mask, role como objetos de Index) se puede construir hoy.

⛔ Lo que sigue bloqueado es la APLICACIÓN de política de celda en el catálogo. Hasta que haya scan planning, la salida vigente es la A (aplicar en el motor, desde la puerta) — con la recomendación de abajo intacta, que ahora vale el doble: constrúyase de forma que mover la llamada al catálogo sea un cambio de sitio, no de diseño.

Recomendación: A, pero construida para que B/C sean un cambio de sitio, no de diseño. El aplicador debe recibir (principal, objetos resueltos, política) y devolver (filtros, máscaras) — sin saber quién lo llama. Así el día que el catálogo planifique, se mueve la llamada y no se reescribe la lógica.

⚠️ Y con una condición innegociable: aplicar en el motor sólo es real si se cierra el vendado directo para tablas con política. Si no, la política se esquiva pidiendo la credencial y leyendo el Parquet. Es F·1, y ya no se puede aplazar.


7·ter · 🏁 F·1 — CONTESTADA POR EL ESTÁNDAR, y mi encuadre era peor

Planteé F·1 como una decisión binaria y global: «¿cerramos el vendado de credencial o renunciamos a la política de fila?». La industria no la plantea así, y su respuesta es mejor.

La frase que lo decide

«Tables with row filters or column masks are NOT supported when using credential
vending for external system access.»
— documentación de Unity Catalog, 2026.

No lo reconcilian ni lo negocian: una tabla con política de celda no se sirve por credencial vendida. El vendado sigue existiendo para todo lo demás.

La decisión no es global: es POR TABLA, y automática. La existencia de una política cambia el modo de servir esa tabla. Eso es mucho más barato que lo que yo había descrito, y no obliga a renunciar a nada en el 95 % del catálogo.

El patrón de los tres, que coincide

QuiénQué hace
Unity CatalogImplementa Iceberg REST con vending… y deshabilita el vending en tablas con filtros o máscaras
Apache PolarisNo soporta filtrado de fila ni máscaras: un rol da la tabla entera o no la da. Para grano fino, «necesitas la capa del motor»
DremioLo dice como arquitectura: el catálogo aplica la frontera GRUESA, el motor aplica el grano FINO, y la metadata aporta la clasificación que alimenta la política
Snowflake HorizonPolíticas nativas en sus queries + una Scan Plan API para extender el grano fino a motores compatibles — el mismo movimiento que el scan planning de Iceberg

⭐ El modelo que se deriva, y encaja con lo que tenemos

   metadata   →  las ETIQUETAS que alimentan la política        (F·3, contrato ODCS)
   catálogo   →  frontera GRUESA: ¿puede ver esta tabla?  + vending SI no hay política
   motor      →  grano FINO: filtros de fila y máscaras de columna

Y la regla de conmutación, que es todo el mecanismo:

Si una tabla tiene política de celda, el catálogo NO vende credencial para ella: su
lectura pasa obligatoriamente por el motor gobernado. Si no la tiene, se sirve como
hoy.

Lo que esto confirma y lo que corrige

  • Confirma la recomendación A de §7·bis (aplicar en el motor): es exactamente lo que hacen Dremio y lo que hacía Snowflake antes de su Scan Plan API.
  • Confirma construirlo para que B/C sea un cambio de sitio: Snowflake y UC están migrando el grano fino hacia el scan planning. Vamos detrás de donde va el estándar, no en contra.
  • ⚠️ Corrige mi encuadre: no hay que «cerrar el vendado». Hay que condicionarlo a la existencia de política, que es un cambio acotado en la cara — donde ya se acuña la credencial (GET …/tables/{table}/credentials).
  • Y deja una obligación: el catálogo tiene que saber qué tablas tienen política para poder negarse. Eso es Index, y refuerza que el modelo de políticas viva ahí (F·2) y no en un servicio aparte.

7·quater · F·2 — el ENCUADRE antes de ejecutar: ¿qué políticas implementamos?

🏁 Decisión del owner (2026-08-12): se prescinde del vending de credenciales para tablas y vistas con política de fila o permisos. Es el patrón de UC y de Snowflake, y de este último también nos alimentamos para el modelo.

7·q·1 · La taxonomía real — son CINCO, no dos

Investigado en Snowflake y Unity Catalog. Yo venía hablando de «filtro de fila y máscara de columna» como si fueran todo; hay tres más, y dos son baratas para nosotros:

TipoQué haceGrano
Máscara de columnaEnmascara o redacta el valor en cada sitio donde la columna aparececolumna
Filtro de filaExpresión booleana sobre columnas de la tabla: TRUE = la sesión puede ver esa filafila
Política de proyecciónDecide si una columna puede aparecer en el resultado — pero se puede seguir usando para filtrar y unircolumna
Política de agregaciónObliga a que la consulta sea agregada, con tamaño mínimo de grupo (anti-reidentificación)tabla
⭐⭐ Etiquetas (governed tags)La política se ata a una etiqueta, no a un objeto. En UC son pares clave-valor sobre catálogos, esquemas, tablas, columnas, modelos y volúmenestransversal

Y la regla de composición, que hay que copiar tal cual: una consulta debe satisfacer TODAS las políticas aplicables, y una fila excluida por el filtro de fila tampoco cuenta en los agregados. Sin esa segunda parte, la agregación se convierte en un canal lateral para leer lo que el filtro ocultó.

7·q·2 · Qué nos conviene a NOSOTROS, y por qué

Aplicado a nuestro sustrato — donde leemos el plan antes de ejecutar (plan-reader), que es una ventaja que Snowflake y UC dan por supuesta y nosotros acabamos de ganar:

Coste para nosotrosValorVeredicto
Máscara de columnaBajo: reescribir la proyección del planAlto (PII)F·2·a — primero
Filtro de filaMedio: inyectar predicado y garantizar que no se esquivaAlto (multi-inquilino, región)F·2·b — segundo
Política de proyecciónBajo, y es el regalo: el plan ya distingue lo PROYECTADO de lo usado en predicados. Para Snowflake es un objeto aparte; para nosotros casi cae soloMedio-alto: permite unir por email sin poder leerlo🟡 F·2·c — barata, entra pronto
Política de agregaciónAlto: exige contar grupos y razonar sobre el resultadoMedio: sólo en escenarios de reidentificaciónfuera del primer corte
⭐⭐ EtiquetasMedio: modelo nuevo en Index + propagación desde la ingestaEl multiplicador: sin ellas, cada política es por objeto y no escala⭐⭐ F·2·0 — antes que ninguna política

Orden: etiquetas → máscara → filtro → proyección. Y la agregación se documenta como conocida y aplazada, no como olvidada.

7·q·3 · Las cuatro decisiones de diseño que hay que tomar en F·2

  1. ¿Expresión inline o función (UDF)? UC exige una UDF registrada con permiso EXECUTE, o una función SQL inline. Una UDF nos obligaría a un registro de funciones en el motor; inline encaja con que nosotros reescribimos el plan. Propuesta: inline primero, y UDF cuando alguien necesite lógica reutilizable.
  2. ¿A quién se aplica? UC usa una cláusula TO con usuarios y grupos. Nosotros tenemos principal_grants con principal_kind — encaja, pero sigue sin haber ni un grant de usuario (§3·2). La política sin sujetos no se puede probar.
  3. ¿Quién puede administrar políticas? En UC hace falta MANAGE sobre el securable, o ser su dueño. Nosotros ya tenemos la familia *_MANAGE_* en los 17 privilegios concedidos: se reutiliza, no se inventa.
  4. ¿Dónde vive el objeto? En Index, como securable de primera clase con dueño y versión — porque el catálogo tiene que saber que la tabla tiene política para negarse a vender credencial (§7·ter). Si vive fuera, esa comprobación es una llamada de red en el camino de toda lectura.

7·q·4 · 🏁 Los cuatro cabos, contestados por el estándar (2026-08-12)

① Inline vs UDF → la pregunta real es otra

UC admite las dos, y añade el matiz que importa: las UDF de SQL son las recomendadas porque el optimizador puede inlinearlas; las de Python también valen pero el optimizador NO puede inlinearlas ni optimizarlas. Y en Snowflake el cuerpo de la política es una expresión SQL — que puede llevar subconsultas, EXISTS y joins.

⇒ El estándar no dice «inline o función»: dice el cuerpo tiene que ser SQL que el optimizador pueda meter dentro del plan.

Para nuestro sustrato: expresión inline desde el principio —es literalmente lo que nuestro aplicador sabe inyectar, porque reescribimos el plan— y si algún día entran funciones, sólo funciones SQL. Una UDF de Python en el camino de lectura sería importar el problema que ellos ya documentan.

② ¿A quién se aplica? → ⭐⭐ NO se enumera en la política: se JOINEA

Éste es el que cambia el diseño. El patrón canónico de Snowflake no mete los sujetos dentro de la política: los deja en una tabla de mapeo / matriz RBAC y la política se une a ella. Su propia recomendación: así se actualizan las entitlements sin tocar la política. Y para la condición recomiendan IS_ROLE_IN_SESSION en vez de CURRENT_ROLE, para que la jerarquía y la herencia de roles funcionen.

Encaja exactamente con lo que ya tenemos: principal_grants es esa tabla de mapeo, y decidePrivilege ya resuelve por cadena, que es la herencia de rol que Snowflake persigue con IS_ROLE_IN_SESSION.

Y desactiva el bloqueo del cabo ②: seguimos necesitando grants de usuario para probar, pero el modelo de políticas no depende de que los sujetos estén enumerados — la política se escribe una vez y los sujetos cambian debajo.

③ ¿Quién administra? → MANAGE o propiedad… y también para MIRAR

UC exige MANAGE sobre el securable o ser su dueño para todas las operaciones de política — y la lista incluye SHOW y DESCRIBE. Más EXECUTE sobre la UDF si se usa una.

⇒ Reutilizamos la familia *_MANAGE_* que ya está en los 17 privilegios concedidos. Y ⚠️ copiamos algo que se nos habría pasado: LISTAR las políticas es una operación privilegiada. Si no, se filtra la forma del modelo de seguridad —qué columnas están enmascaradas, qué tablas tienen filtro— a quien no debería verla.

④ ¿Dónde vive? → etiqueta arriba, política en el objeto… y parametrizada

En UC las etiquetas gobernadas son de nivel cuenta y se aplican a catálogos, esquemas, tablas, columnas, modelos y volúmenes; la política se ata a catálogo, esquema o tabla. En Snowflake las políticas son objetos de nivel esquema.

⭐⭐ Y el hallazgo de diseño: las funciones de introspección de etiqueta extraen el VALOR de la etiqueta y se lo pasan a la función (cláusula USING COLUMNS), de modo que una sola política y una sola función cubren MUCHOS valores de etiqueta, en vez de una política por valor.

⇒ Para nosotros: la etiqueta vive a nivel workspace (nuestro equivalente de cuenta), la política se ata al securable, y el valor de la etiqueta es un PARÁMETRO de la política, no una política nueva. Es lo que mantiene el número de políticas bajo — y es justo el error que habríamos cometido: una política por cada etiqueta.

7·q·5 · Lo que NO se copia

  • La sintaxis CREATE POLICY no es prioritaria: la superficie del cliente es F·5, y llegar por SQL antes que por UI invertiría el objetivo del producto.
  • Las políticas sobre modelos y volúmenes de UC: nuestro catálogo aún no las gobierna igual. Entra cuando entren.

7·sexies · ⭐⭐⭐ LOS GRANTS DE USUARIO — una pieza que falta en CINCO sitios

Intuición del owner (2026-08-12), y al comprobarla resulta más fuerte de lo que parecía: los grants no son «otra cosa de gobernanza» que compite con el producto. Son la pieza que bloquea cinco frentes distintos, y por eso cada uno se atasca en un sitio que parece propio y no lo es.

FrenteDónde se atascaPor qué es lo MISMO
Los verbosDML_VERBS_PERMITIDOS prohíbe verbos globalmenteExiste porque no se puede decir «este usuario puede borrar de esta tabla». Es una prohibición global que sustituye a una decisión por objeto
Las políticas de celdaNo se pueden ni probarUna política sin sujetos no tiene a quién aplicarse (§7·q·4·②)
La superficie del cliente (F·5)No existe«Que el cliente defina sus roles» es administrar grants
Las ActionsTienen su propio modelo (can_execute_action_type)Se inventó porque el de verdad no llegaba a los usuarios. Dos motores de permisos por la misma causa
Los erroresNo se puede elegir entre «no existe» y «no puedes»La forma del error es una decisión de autorización (errores-gobernados-approach.md §7)

Una sola pieza ausente explica cinco síntomas. Y explica también por qué el
paradigma legacy inventó su propio permiso: cuando el modelo de autorización no
alcanza, cada superficie se fabrica el suyo.

Y por eso no es «gobernanza antes que producto»

Es al revés de como suena: el plano de gobernanza ES el sustrato del producto. Sin él, toda superficie nueva —la UI de políticas, el editor, las Actions, los agentes— vuelve a inventarse su propio modelo de permisos, que es exactamente el error que estamos retirando.

⚠️ Y la trampa contraria, que también hay que evitar

De «todo depende de los grants» no se sigue «hagamos un proyecto enorme de grants». Eso sería el mismo error con el signo cambiado. El paso mínimo se rige por el principio rector del §2: los grants tienen que DERIVAR de algo, no declararse.

① DERIVAR el grant inicial de lo que YA existe:
     workspace_members (rol)  +  propiedad del dataset  →  grants de usuario
   No es una migración: es una proyección de datos que ya tenemos.

② ENCENDER la comprobación para usuarios, con CONTROL NEGATIVO:
     un usuario SIN grant recibe una denegación de verdad, no un permiso por omisión.

③ Y sólo entonces, empezar a estrechar `DML_VERBS_PERMITIDOS` POR OBJETO
     en vez de abrir verbos globalmente.

⇒ Es el mismo movimiento que hicimos con el paradigma de nombres: no declarar, derivar. Un grant declarado a mano para 8 inquilinos sería el legacy otra vez; un grant derivado de la membresía y la propiedad ya es verdad hoy y sólo hay que materializarlo.

Pasa a ser F·2·0 — antes que el modelo de políticas, porque sin sujetos ese modelo no se puede probar; y antes que P·4, porque la lista de verbos no se retira borrando locateDmlCommand: se retira cuando hay a quién conceder.


7·septies · El paradigma MADURO de grants — qué dicen los líderes

Investigado en Snowflake y Unity Catalog (2026-08-12). Seis reglas, y las tres últimas nos faltan enteras.

① Los grants NUNCA van a usuarios — van a ROLES o GRUPOS

  • Snowflake: «Snowflake no ata privilegios directamente a usuarios. Los usuarios heredan roles, y los roles llevan los grants.»
  • UC: usa grupos para conceder acceso y para asignar la propiedad de la mayoría de securables. Y una regla operativa explícita: «no modifiques los grupos en Databricks: usa tu IdP» (SCIM).

Para nosotros: el sujeto de un grant tiene que ser un rol, no un user_id. principal_grants ya tiene principal_role y catalog_role, así que la forma está. Y los grupos derivan de Clerk —organización y rol—, no se administran en el catálogo: exactamente el §2, derivar en vez de declarar.

② La PROPIEDAD es de primera clase… y es de un ROL, no de una persona

  • Snowflake: «Los roles poseen objetos, no los usuarios. Cuando Kim crea una tabla usando data_engineer_role, ese rol es el dueño.»
  • UC: el dueño tiene automáticamente todos los privilegios sobre ese objeto — y ⚠️ la propiedad NO se hereda hacia abajo.

⇒ ⛔ Gap nuestro: datasets.created_by es un usuario. En el modelo maduro sería un rol. Es un cambio de forma pequeño con consecuencias grandes: cuando esa persona se va, el objeto no se queda huérfano.

③ La herencia baja por la jerarquía… con una excepción que habríamos fallado

UC: los privilegios se heredan hacia abajo por la jerarquía de securables. PERO los grants a nivel metastore no se heredan a los hijos: gobiernan operaciones de ámbito metastore (CREATE CATALOG), no el acceso a los datos.

⇒ ⚠️ Es justo el error que íbamos a cometer: dar un grant a nivel workspace y que implicara acceso a todo lo de dentro. El grant de workspace gobierna operaciones DE workspace, no el dato.

④ ⭐⭐ FUTURE GRANTS — cómo un objeto nace ya con sus permisos

Snowflake permite definir el juego inicial de privilegios sobre los objetos que se creen en un esquema (p. ej. SELECT sobre todas las tablas futuras de myschema para un rol).

Esto ES el mecanismo de «desde la ingesta» que buscábamos, y es derivación pura: un dataset ingerido en un esquema hereda los grants futuros de ese esquema, sin paso manual y sin que nadie tenga que acordarse. ⛔ No tenemos nada equivalente.

⑤ Separación de deberes: conceder es un oficio distinto de crear

  • Snowflake: USERADMIN crea usuarios y roles · SECURITYADMIN gestiona los grants · SYSADMIN crea bases, esquemas y tablas. «Mantén fronteras claras entre los roles que gestionan roles y los que gestionan datos.»
  • UC: sólo los admin del metastore, los dueños, y quien tenga MANAGE sobre el objeto pueden conceder.

⇒ ⛔ Gap nuestro: hoy el dueño del workspace lo hace todo. Quién puede conceder es en sí un privilegio, y debe poder separarse de quién crea datos.

⑥ El sustrato: metadata del catálogo + identidad del IdP. Ningún servicio nuevo

Ni Snowflake ni UC montan un servicio de políticas aparte: los grants son metadata del catálogo, y la identidad y los grupos vienen del proveedor de identidad.

⇒ Encaja con lo que tenemos y confirma no meter una pieza más: los grants viven en Index (ya están), la identidad y los grupos en Clerk (ya están). OpenFGA entra después, como decisor de esa retícula cuando las relaciones superen a las filas — no como almacén paralelo.

🧭 Cómo casa con el resto del plano

IdP (Clerk)   →  usuarios y grupos                        identidad
ROLES         →  a quién se concede                       nunca a personas
GRANTS        →  privilegio × securable, con herencia     grano GRUESO: ¿puedes tocar el objeto?
    ↓                                                      + FUTURE GRANTS para lo que nazca
POLÍTICAS     →  filtro de fila · máscara · proyección    grano FINO: ¿qué ves DENTRO?
ACTIONS       →  operación tipada sobre un objeto         «¿puede este rol ESTA operación aquí?»

Las Actions no necesitan un modelo de permisos propio: necesitan que la pregunta «¿puede este rol hacer esta operación sobre este objeto?» tenga respuesta — que es exactamente lo que da esta retícula, y lo que can_execute_action_type se fabricó por su cuenta al no tenerla.

Los tres gaps, en orden de lo que desbloquean

GapQué desbloquea
1El sujeto es un rol, derivado de Clerk (hoy no hay ni un grant de usuario)Todo lo demás
2Future grants por esquema«Desde la ingesta» sin paso manual
3La propiedad es de un rol y conceder es un privilegio aparteQue el cliente administre lo suyo sin poder pisar lo ajeno

7·octies · 🏁 F·2·0 — LOS GRANTS DE USUARIO, DERIVADOS (2026-08-12)

Los 368 grants de servicio pasan a tener compañía: 12 sujetos humanos, ninguno declarado a mano. Y el orden fue el del principio rector: ⓪ medir el insumo → ① derivar → ② encender con control negativo → ③ future grants.

7·o·0 · ⓪ El censo — y tres hallazgos que cambiaron el diseño

scripts/governance/g0-censo-de-sujetos.ts

12 usuarios · 3 workspaces con miembros · roles reales: owner 3 · admin 6 · viewer 3
                                          `editor` y `guest`: CERO filas
123 datasets · 109 con dueño humano · principal_grants: 368, de usuario 0
HallazgoQué decidió
Los 109 datasets con dueño humano los creó el owner de su workspaceDerivar de la PROPIEDAD no concedería ni un privilegio nuevo: el owner ya lo recibe por membresía ⇒ la propiedad sale del primer corte
14 datasets tienen por dueño un SERVICIO (lakekeeper-spark-editor, f2-canary)datasets.created_by no es «el dueño»: es «quién lo tecleó». Derivar propiedad de esa columna metería servicios como sujetos humanos
⚠️Los 7 catalog roles ya existen en los 8 inquilinos (31 grants cada uno)La derivación presta paquetes que ya existen — no inventa ni un privilegio

§7·septies·② (la propiedad) queda medido y APLAZADO, no olvidado: entra cuando exista una columna que signifique dueño y sea de un rol.

7·o·1 · ① La derivación — una PROYECCIÓN, no una migración

lib/governance/membership-grants.ts (puro) + …-queries.ts (I/O)

usuario ──▶ principal role (`ws-admin`) ──▶ catalog role (`writer`) ──▶ grant
 (Clerk)     ← esto es lo que se DERIVA      ← esto ya existía (plantilla)
MembresíaRol derivadoPrestaPor qué
ownerws-owneroperatorEl único con WORKSPACE_MANAGE_ACCESS
adminws-adminwriterAdministra el dato entero y no puede repartirse permisos
editorws-editorreader + dml-writerLa misma composición que el SQL Editor
viewerws-viewerreaderMetadata y datos. Nada de escritura
guestws-guestcatalog-browserRecorre; no abre ninguna tabla

⭐⭐ owner y admin NO reciben lo mismo, y ahí está la separación de deberes (§7·septies·⑤): conceder es un oficio distinto de crear. Clerk ya distingue los dos nombres, así que implementarla no costó nada — sólo verlo.

⚠️ guest es la única línea que no sale del dato, y se dice. useWorkspaceRole afirma «todos pueden ver», que daría reader; pero eso es el permiso de la UI, no el del dato. Hoy la decisión es inocua —0 miembros guest— y por eso era el momento barato de tomarla.

Y es proyección porque reconcilia en las DOS direcciones. planDerivacion calcula alta y baja: degradar de admin a viewer quita ws-admin. Sin esa mitad esto sería un volcado con fecha de caducidad. La reconciliación está cableada donde la membresía cambia —los tres handlers de app/api/clerk-webhook/route.ts— y en el alta (tenant-provisioning.ts ③·bis).

Aplicado: g1-derivar-grants-de-usuario.ts --apply → 12 altas · 0 bajas · 24 verdes / 0 rojos, con control negativo.

7·o·2 · ② El aplicador — y por qué nace apagado

lib/governance/user-authz.ts, cableado en junction.ts (lectura, escritura y CREATE).

La cara Iceberg REST pregunta «¿puede este MOTOR?». Esto pregunta «¿puede
esta PERSONA?»
— que no se preguntaba en ningún sitio.

⭐⭐⭐ Y el guardián del CREATE llevaba su condición de retirada escrita, y caducó. Decía literalmente: «principal_grants tiene 360 filas y todas son de servicios; autorizar con decidePrivilege(usuario,'TABLE_CREATE') denegaría a todo el mundo». Era cierto hasta que ①. Es la tercera vez que una lista de prohibiciones se retira releyendo su propio motivo (regla 49).

Interruptor USER_AUTHZ con tres modos —off (default) · shadow · enforce—, el mismo idioma que modoDelAuditor y modoDelGuardian. Nace en off porque el modo de fallo es asimétrico: un permiso de más no se nota, uno de menos tira el producto.

Gate: g2-aplicador-de-usuario.ts17 verdes / 0 rojos.

7·o·3 · ③ Future grants — ⭐⭐ NO HACEN FALTA

scripts/governance/g3-future-grants.ts → 5/5

Snowflake necesita GRANT … ON FUTURE TABLES IN SCHEMA por una razón de su modelo: allí un grant sobre un esquema no alcanza a sus tablas. El nuestro hereda por la cadena, así que la pregunta era si eso ya cubre lo futuro. Medido:

① 123 datasets · TODAS las cadenas nacen en su workspace
② 168 grants alcanzables desde los roles derivados · sobre `dataset` suelto: 0
③ cadena workspace → catalog → schema → dataset · el permiso llega HEREDADO

La herencia por la cadena YA ES el future grant. Un dataset ingerido en un
contenedor concedido nace cubierto, sin paso manual y sin mecanismo nuevo.

⚠️ Lo que sí hace falta es el INVARIANTE que lo sostiene, y por eso el gate se queda: los grants derivados se conceden sobre CONTENEDORES. El día que uno se conceda sobre un dataset suelto, ese rol deja de cubrir lo futuro en silencio.

§7·septies·④ se cierra sin construir la pieza, que es el mejor resultado posible: preguntarle al estándar salió barato (regla 53), y comprobar si su problema es el nuestro salió gratis.

7·o·4 · Lo que queda abierto de F·2·0

Encender (USER_AUTHZ=shadowenforce en Vercel)Es la mitad que convierte los grants en gobierno. Exige despliegue ⇒ decisión del owner
⚠️ El canary del camino enterog2 mide la función que la puerta llama, no el HTTP. «Un health-check dice que el proceso vive; sólo un e2e que ESCRIBA dice que el camino existe»
🟡 La propiedad como eje (§7·septies·②)Aplazada con motivo medido (§7·o·0)
🟡 Estrechar DML_VERBS_PERMITIDOS por objetoAhora ya hay a quién conceder. Es P·4

8 · Bitácora

Una entrada por sesión. Lo que se midió, lo que se decidió, y lo que cambió de opinión.

2026-08-12 · sesión 1 — el encuadre

  • Medido: Iceberg 1.11.0 en el cluster · retícula existente y poblada sólo de servicios (368/368) · plano ontológico legacy con 4 ejecuciones · Actions escribiendo fuera de la puerta con su propio modelo de permisos.
  • Decidido: el plano ontológico legacy se rehace · OpenMetadata deja de ser fuente de verdad · la fricción de Iceberg no existe (es de dónde aplicamos).
  • Cambió de opinión: la proyección no es el centro — el centro es el espacio de acción acotado (corrección del owner) · «no dependeremos de scan planning servidor» pasa a estar en revisión.
  • 🏁 F·0 contestada: NO. Lakekeeper declara 25 endpoints y ninguno de planificación (§7·bis). ⇒ la política de celda no puede aplicarse hoy en el catálogo, y la decisión del vendado (F·1) pasa de importante a bloqueante.
  • ⚠️ Trampa de instrumento, propia: la sonda de F·0 usó primero una coordenada LÓGICA (main.default.diets) contra el catálogo FÍSICO, que es opaco desde N·3 — y el 404 de la tabla se confundía con el 404 de la ruta. Se separó el veredicto: se apoya en endpoints[], no en el POST.
  • 🏁 F·1 contestada, y por el estándar, no por criterio propio (§7·ter). UC deshabilita el vending en tablas con filtros o máscaras; Polaris no tiene grano fino y manda a la capa del motor; Dremio lo declara como arquitectura (catálogo = frontera gruesa, motor = grano fino, metadata = clasificación). ⇒ el patrón es de tres capas.
  • ⚠️ Y me corrigió el encuadre: planteé F·1 como binaria y GLOBAL («cerrar el vendado o renunciar a la política»). Es por tabla y automática: la existencia de política conmuta el modo de servir esa tabla. Mucho más barato, y no obliga a renunciar a nada en el resto del catálogo.
  • Fuentes de las fuentes: las fuentes de §10 con las notas de UC/Polaris/Dremio/ Snowflake.
  • 🏁 Decidido por el owner: se prescinde del vending para tablas y vistas con política de fila o permisos.
  • Encuadre de F·2 medido (§7·quater): son CINCO tipos de política, no dos. Se me habían quedado fuera la política de proyección (ver la columna en un JOIN pero no en el resultado) y la de agregación (tamaño mínimo de grupo). Y la regla de composición que hay que copiar entera: una fila excluida por el filtro tampoco cuenta en los agregados — sin eso, la agregación es un canal lateral.
  • Y una ventaja nuestra que ellos dan por supuesta: como leemos el PLAN antes de ejecutar, la política de PROYECCIÓN nos sale casi gratis — el plan ya distingue lo proyectado de lo usado en predicados. En Snowflake es un objeto aparte.
  • 🏁 Los 4 cabos de F·2, contestados por el estándar (§7·q·4). Dos cambian el diseño: los sujetos NO se enumeran en la política, se JOINEAN a una tabla de mapeo —que para nosotros ya existe, principal_grants, con herencia por cadena— y el valor de la etiqueta es un PARÁMETRO de la política, no una política nueva por valor. Ese segundo era el error que íbamos a cometer.
  • ⚠️ Y uno que se nos habría pasado entero: listar políticas es una operación privilegiada (SHOW/DESCRIBE exigen MANAGE), porque el catálogo de políticas filtra la forma del modelo de seguridad.
  • ⭐⭐⭐ Los grants de usuario son la pieza que falta en CINCO sitios (§7·sexies), y eso reordena el trabajo: verbos, políticas, superficie del cliente, Actions y errores se atascan en sitios que parecen propios y son el mismo. Una pieza ausente, cinco síntomas — y explica por qué el legacy se fabricó su propio permiso: cuando el modelo de autorización no alcanza, cada superficie inventa el suyo.
  • ⚠️ Y la trampa contraria, anotada: de ahí NO se sigue «un proyecto enorme de grants». El paso mínimo se rige por el §2 — derivar, no declarar: el grant inicial sale de workspace_members + propiedad del dataset, que ya es verdad hoy y sólo hay que materializarla. Pasa a ser F·2·0.

2026-08-12 · sesión 2 — F·2·0, ejecutada

  • 🏁 F·2·0 HECHA (§7·octies), en el orden del principio rector: ⓪ censo → ① derivar → ② encender con control negativo → ③ future grants.
  • Medido antes de derivar, y cambió el diseño: los 109 datasets con dueño humano los creó el owner ⇒ derivar de la propiedad no concedería nada nuevo; y 14 datasets tienen por dueño un SERVICIO ⇒ ⛔ created_by no es «el dueño», es «quién lo tecleó». La propiedad sale del primer corte con motivo, no por olvido.
  • 12 grants de usuario, ninguno declarado: proyección de workspace_members sobre la cadena que ya existía. principal_grants pasa de 368 filas y cero sujetos humanos a tener sujetos.
  • ⭐⭐ La separación de deberes salió gratis: Clerk ya distingue owner de admin, así que dar WORKSPACE_MANAGE_ACCESS sólo al primero no costó diseño, sólo verlo. admin administra el dato y no puede repartirse permisos.
  • ⭐⭐⭐ Y el guardián del CREATE llevaba su condición de retirada escrita, y caducó — tercera vez que pasa (regla 49). Decía «denegaría a todo el mundo porque no hay grants de usuario»; ① lo invalidó.
  • ⭐⭐ F·2·0·③ se cierra SIN construir la pieza: los future grants de Snowflake resuelven un problema de su modelo (allí un grant de esquema no alcanza a las tablas). El nuestro hereda por la cadena ⇒ la herencia YA es el future grant, medido sobre los 123 datasets. Lo que se construye en su lugar es el invariante: los grants derivados van sobre CONTENEDORES, con su gate.
  • ⚠️ Dos trampas de medición propias, y la misma las dos veces. El gate de ① y el de ② salieron verdes sin haber medido el caso viewer: el único inquilino con datasets sólo tiene owner y admin. Una sonda que no llega al caso no lo aprueba, lo ignora — y el resultado se lee igual. Se cerró preguntando por la cadena del workspace (①) y por crea, que es la única intención que no necesita dataset (②).
  • ⚠️ Y un bug propio que cazó su test: el normalizador quitaba el prefijo org: antes de bajar a minúsculas, así que un ORG:Admin caía en el default. En la UI es latente; aquí el default deniega ⇒ un admin habría perdido la escritura.
  • Estado: USER_AUTHZ nace en off. Los grants existen y todavía no gobiernan: encender es despliegue y decisión del owner.
  • Verde: 788 tests (51 ficheros) · tsc sin errores nuevos · gates 24/24 · 17/17 · 5/5.

9 · Riesgos

  1. Esto es producto nuevo, no una fase más: modelo, API y UI. Compararlo con «añadir un verbo» sería engañarnos.
  2. La política de celda cuesta rendimiento: planificar en servidor añade un salto en el camino de toda lectura.
  3. Si Lakekeeper no implementa el scan planning, la alternativa es aplicar en el motor — y vuelve el impuesto por motor, o hay que cambiar de catálogo.
  4. Una etiqueta mal puesta gobierna mal. Toda clasificación automática necesita revisión humana antes de gobernar.
  5. El estándar de intercambio de políticas no existe todavía — los trabajos siguen abiertos y la gobernanza sigue fragmentada entre fabricantes. Por eso el modelo debe ser el de UC: para que migrar mañana sea traducir, no rehacer.

10 · Fuentes