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 unsustrato-07limpio. 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.mddonde 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:
| Pregunta | Ejemplo | |
|---|---|---|
| ① 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 OSS | La 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 uno | Su metastore, su runtime, su modelo de identidad | El 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 |
| Foundry | La forma de Action: operación de escritura tipada, validada, con permisos y auditada, sobre un tipo de objeto | Su implementación (es lo que copió el legacy) · su idea de que el grafo es el producto | La 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 / Bitol | El contrato de activo: propiedades con semántica de negocio y atributos de gobernanza (esto es PII, sólo estos roles) | Su tooling de vendors | El 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 dato | El producto, salvo que la reescritura salga cara de hacer | El grafo se proyecta desde el catálogo y lee por la puerta ⇒ hereda política sin implementarla |
| OpenFGA | ReBAC: relaciones en vez de filas (equipos, propiedad, compartición, y gobernar el traversing) | Ser una segunda fuente de política | Es el decisor de la retícula que ya existe, no una retícula nueva |
| Apache Egeria | Vocabulario de gobernanza: governance actions, clasificaciones, zonas | El runtime (cohortes Kafka) — desproporcionado | Se saquea el diccionario, se implementa sobre Index |
| Apache Atlas | Su sistema de tipos (entidades, relaciones, clasificaciones, atributos), que fue pionero | Todo el producto: HBase + Solr + Kafka, era Hadoop | Se 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 |
| El patrón: autorizar desde el plan analizado, con objetos ya resueltos | El producto: extensión JVM + su propio almacén de políticas | Ya 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:
- ¿Qué problema nuestro resuelve, medido? (no «qué hace bien»)
- ⭐ ¿De qué dato NUESTRO deriva? Si la respuesta es «se declara», es el legacy otra vez con otro nombre.
- ¿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ón | CATALOG_OPS · decidePrivilege (herencia por cadena) |
| Una retícula con forma de securable, poblada | principal_grants: 368 filas |
| El plan del motor con objetos resueltos, privilegio y rol destino/fuente | P·0/P·1 · plan-reader |
| Verbos gobernados con destino resuelto por el plan, y reconciliación de Index | ALTER TABLE … ADD COLUMN en producción (2026-08-12) |
| Iceberg 1.11.0 en el cluster ⇒ el cliente de scan planning está puesto | iceberg-spark-runtime-4.1_2.13-1.11.0.jar |
| Coordenada estricta y namespace opaco | sustrato-06 §7·ter · N·5 |
3·2 · Lo que NO hay
| Evidencia | |
|---|---|
| Roles y políticas definidos por el CLIENTE | no existe superficie |
| Nivel de celda (fila/columna) | Iceberg no lo da por diseño (§4) |
| Grants de usuario | 368 de 368 son de servicios |
| Etiquetas sobre las que escribir política | no existen |
| Política desde la ingesta | la ingesta escribe sin contrato ni etiquetas |
| Un solo modelo de tipos y permisos | hay 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 correcta —target_object_type_id, can_execute_action_type,
contrato, versiones, trazas, denial_context— y dos defectos que lo invalidan:
executeObjectActionType/syncOutputToOntologyescriben en PG sin pasar por la puerta del Warehouse.can_execute_action_typees su propio modelo de permisos (role + submission), distinto deprincipal_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 directo | Incompatible 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 uno | El modelo maduro concede sobre etiquetas. La tabla no se tira: se le añade el eje |
| 🟡 | Karma con índice y planificación propios | Quien planifica en cliente se salta la política |
| 🟡 | OpenMetadata como fuente de verdad | El 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
| Concepto | Por qué nos afecta |
|---|---|
| ABAC vs RBAC vs ReBAC | La política madura se escribe sobre etiquetas, no sobre objetos. Es lo que hace que N tablas nuevas no cuesten N políticas |
| Etiquetas / clasificaciones | El sustantivo de la política. Nacen en la ingesta |
| Row filter y column mask como objetos de primera clase | Versionados, con dueño y auditoría. La diferencia entre una política y una costumbre |
| Server-side scan planning | El punto donde la política deja de depender del motor |
| Credential vending vs enforcement | La 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 access | Principal.purpose existe y no se usa |
| SHACL | Validación de formas del grafo (lo que se proyecta), frente a ODCS (lo que entra) |
7 · El plan
| Qué | Gate | Estado | |
|---|---|---|---|
| 🏁 F·0 | ¿Implementa Lakekeeper planTableScan? → NO (§7·bis) | scripts/warehouse/f0-scan-planning.ts | HECHO 2026-08-12 |
| 🏁 F·1 | El vendado de credencial cuando hay política → lo contesta el estándar, no nosotros (§7·ter) | la práctica de UC/Polaris/Dremio/Snowflake | HECHO 2026-08-12 |
| 🏁 F·2·0 | LOS 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 --apply → 24/24 · g2-aplicador-de-usuario.ts → 17/17 · g3-future-grants.ts → 5/5 · 20 tests puros | HECHO 2026-08-12 · queda encender (USER_AUTHZ=enforce) |
| F·2 | El modelo de políticas con el vocabulario de UC: tag, row_filter, column_mask, role como objetos de Index | esquema + 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·3 | Las etiquetas nacen en la ingesta (contrato ODCS) | un dataset ingerido llega con sus etiquetas | ⏳ |
| F·4 | El aplicador: filtro y máscara donde se decidió en F·0/F·1 | un usuario ve menos filas y no puede esquivarlo · con control negativo | ⏳ |
| F·5 | La superficie del cliente: sus roles y sus políticas | un cliente crea rol y política sin que intervenga nadie | ⏳ |
| F·6 | Unificar el plano de Actions con el del Warehouse: un modelo de tipos, una retícula, escritura por la puerta | una Action hereda la política de celda sin implementarla | ⏳ |
| F·7 | El 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
- ⛔ 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.
- ⭐ Y
GET …/tables/{table}/credentialsSÍ 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é | Coste | Riesgo | |
|---|---|---|---|
| A · Aplicar en el MOTOR | Inyectar 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·5 | Impuesto 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 Lakekeeper | Implementar planTableScan en un catálogo Rust ajeno | Alto y con calendario ajeno | Es el sitio correcto, pero no controlamos su hoja de ruta |
| C · Cambiar de catálogo | A uno que lo implemente | Migración del control-plane entero |
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:
- La cara no enruta
/scan(routeCatalogRequestno lo conoce). - La cara anuncia
endpoints: []y no publicascan-planning-mode, así que ningún motor sabe que puede delegar la planificación. - El
PlanTableScanRequestllevafilteryselect: ahí es donde la cara inyectaría el predicado del aplicador y recortaría columnas. - ⚠️ 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/configes 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én | Qué hace |
|---|---|
| Unity Catalog | Implementa Iceberg REST con vending… y deshabilita el vending en tablas con filtros o máscaras |
| Apache Polaris | No 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» |
| Dremio | Lo 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 Horizon | Polí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:
| Tipo | Qué hace | Grano |
|---|---|---|
| Máscara de columna | Enmascara o redacta el valor en cada sitio donde la columna aparece | columna |
| Filtro de fila | Expresión booleana sobre columnas de la tabla: TRUE = la sesión puede ver esa fila | fila |
| ⭐ Política de proyección | Decide si una columna puede aparecer en el resultado — pero se puede seguir usando para filtrar y unir | columna |
| Política de agregación | Obliga 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úmenes | transversal |
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 nosotros | Valor | Veredicto | |
|---|---|---|---|
| Máscara de columna | Bajo: reescribir la proyección del plan | Alto (PII) | ⭐ F·2·a — primero |
| Filtro de fila | Medio: inyectar predicado y garantizar que no se esquiva | Alto (multi-inquilino, región) | ⭐ F·2·b — segundo |
| ⭐ Política de proyección | Bajo, 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 solo | Medio-alto: permite unir por email sin poder leerlo | 🟡 F·2·c — barata, entra pronto |
| Política de agregación | Alto: exige contar grupos y razonar sobre el resultado | Medio: sólo en escenarios de reidentificación | ⏳ fuera del primer corte |
| ⭐⭐ Etiquetas | Medio: modelo nuevo en Index + propagación desde la ingesta | El 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
- ⭐ ¿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. - ¿A quién se aplica? UC usa una cláusula
TOcon usuarios y grupos. Nosotros tenemosprincipal_grantsconprincipal_kind— encaja, pero sigue sin haber ni un grant de usuario (§3·2). La política sin sujetos no se puede probar. - ¿Quién puede administrar políticas? En UC hace falta
MANAGEsobre el securable, o ser su dueño. Nosotros ya tenemos la familia*_MANAGE_*en los 17 privilegios concedidos: se reutiliza, no se inventa. - ¿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 POLICYno 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.
| Frente | Dónde se atasca | Por qué es lo MISMO |
|---|---|---|
| Los verbos | DML_VERBS_PERMITIDOS prohíbe verbos globalmente | Existe 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 celda | No se pueden ni probar | Una 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 Actions | Tienen 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 errores | No 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:
USERADMINcrea usuarios y roles ·SECURITYADMINgestiona los grants ·SYSADMINcrea 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
MANAGEsobre 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
| Gap | Qué desbloquea | |
|---|---|---|
| 1 | El sujeto es un rol, derivado de Clerk (hoy no hay ni un grant de usuario) | Todo lo demás |
| 2 | Future grants por esquema | «Desde la ingesta» sin paso manual |
| 3 | La propiedad es de un rol y conceder es un privilegio aparte | Que 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
| Hallazgo | Qué decidió | |
|---|---|---|
| ⭐ | Los 109 datasets con dueño humano los creó el owner de su workspace | Derivar 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ía | Rol derivado | Presta | Por qué |
|---|---|---|---|
owner | ws-owner | operator | El único con WORKSPACE_MANAGE_ACCESS |
admin | ws-admin | writer | Administra el dato entero y no puede repartirse permisos |
editor | ws-editor | reader + dml-writer | La misma composición que el SQL Editor |
viewer | ws-viewer | reader | Metadata y datos. Nada de escritura |
guest | ws-guest | catalog-browser | Recorre; 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.ts → 17 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=shadow → enforce en Vercel) | Es la mitad que convierte los grants en gobierno. Exige despliegue ⇒ decisión del owner |
| ⚠️ El canary del camino entero | g2 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 objeto | Ahora 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 enendpoints[], 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
JOINpero 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/DESCRIBEexigenMANAGE), 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_byno 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_memberssobre la cadena que ya existía.principal_grantspasa de 368 filas y cero sujetos humanos a tener sujetos. - ⭐⭐ La separación de deberes salió gratis: Clerk ya distingue
ownerdeadmin, así que darWORKSPACE_MANAGE_ACCESSsólo al primero no costó diseño, sólo verlo.adminadministra el dato y no puede repartirse permisos. - ⭐⭐⭐ Y el guardián del
CREATEllevaba 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 tieneowneryadmin. 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 porcrea, 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 unORG:Admincaía en el default. En la UI es latente; aquí el default deniega ⇒ un admin habría perdido la escritura. - Estado:
USER_AUTHZnace enoff. Los grants existen y todavía no gobiernan: encender es despliegue y decisión del owner. - Verde: 788 tests (51 ficheros) ·
tscsin errores nuevos · gates 24/24 · 17/17 · 5/5.
9 · Riesgos
- Esto es producto nuevo, no una fase más: modelo, API y UI. Compararlo con «añadir un verbo» sería engañarnos.
- La política de celda cuesta rendimiento: planificar en servidor añade un salto en el camino de toda lectura.
- 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.
- Una etiqueta mal puesta gobierna mal. Toda clasificación automática necesita revisión humana antes de gobernar.
- 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
- Securing Iceberg con control de acceso de fila/columna · Gobernanza de tablas Iceberg
- Iceberg 1.11.0 — novedades · Announcing Iceberg 1.11.0 · Scan planning en el REST catalog
- Row filters y column masks — Databricks · Unity Catalog y la nueva era de Iceberg
- Comparativa de catálogos · Estado de los catálogos Iceberg, junio 2026
- Open Data Contract Standard (Bitol / LF AI & Data) · Data contracts en OpenMetadata
- Ontop — Virtual Knowledge Graph
- Credential vending de Unity Catalog para acceso externo · Credential vending en Dremio · Securing your Iceberg lakehouse (Dremio) · Snowflake Horizon vs Unity Catalog
- Taxonomía de políticas: row access policies · aggregation policies · projection y masking en la práctica · ABAC en Unity Catalog — conceptos · crear políticas ABAC · ABAC, governed tags y clasificación, ya GA
- Los 4 cabos: usar row access policies · CREATE ROW ACCESS POLICY · row access policies con tabla de mapeo · crear y gestionar políticas ABAC · tutorial de ABAC
- Paradigma de grants: Snowflake — buenas prácticas de control de acceso · visión general del control de acceso · UC — buenas prácticas · UC — gestionar privilegios · UC — modelo de permisos