Saltar al contenido

060 — Control de acceso: privilegio mínimo, roles y seguridad por fila

🗂️ parte 🎚️ nivel ⏱️ duración 📗 clase

Programa · Parte 11 · ← Anterior · Siguiente →

Parte 11 — Operación, seguridad y gobierno · Intermedio · 3 horas estimadas · motores postgresql, mysql · laboratorio labs/03-transactions · 4 fuentes.

Conceptos centrales: privilegio mínimo · rol · seguridad por fila · separación de funciones

En este caso se comparan 7 motores: 6 lo resuelven (5 con el resultado comprobado por máquina) y 1 no, con el motivo escrito.

De qué trata esta clase

El control de acceso con una comprobación incómoda como punto de partida: si la aplicación se conecta como propietaria del esquema, no hay privilegio mínimo. Trata roles, separación de funciones y seguridad por fila, cuya ventaja sobre filtrar en la aplicación es que no hay consulta que se pueda olvidar del filtro.

flowchart LR
    C["🗄️ Clase 060"]
    C --> K1["privilegio mínimo"]
    C --> K2["rol"]
    C --> K3["seguridad por fila"]
    C --> K4["separación de funciones"]
    classDef raiz fill:#0b3d2e,stroke:#3fb950,color:#fff
    class C raiz

Antes de empezar

Esta clase supone que ya trabajaste lo siguiente. Si algo de la última columna no te suena, vuelve a esa clase antes de seguir: aquí se usa sin volver a explicarlo.

# Clase previa Lo que se da por sabido
013 Independencia de datos y los tres niveles de esquema esquema conceptual · esquema físico · vista externa · independencia lógica
024 DDL: el esquema como contrato ejecutable tipo de dato · restricción · valor por defecto · DDL transaccional

Vocabulario de la clase

Los términos que siguen se usan más adelante con este significado exacto. La definición completa, con sus términos relacionados, está en el glosario del programa.

Término Qué significa Procedencia
privilegio mínimo Cada identidad recibe exactamente los permisos que necesita para su función y ninguno más. La comprobación práctica es incómoda y reveladora: si la aplicación se conecta como propietaria del esquema, no hay privilegio mínimo. se introduce aquí
rol Agrupación de privilegios que se concede a personas o a aplicaciones. Permite razonar sobre permisos por función en lugar de por individuo, y revocar el acceso de alguien sin tener que auditar cada objeto. se introduce aquí
seguridad por fila Políticas que el motor añade automáticamente a cada consulta para que un usuario solo vea las filas que le corresponden. La ventaja sobre filtrar en la aplicación es que no hay consulta que se pueda olvidar del filtro. se introduce aquí
separación de funciones Que quien desarrolla no sea quien despliega en producción, y que quien opera no pueda borrar sus propias huellas de auditoría. Es un control organizativo antes que técnico, y sin él el registro de auditoría no prueba nada. se introduce aquí

Propósito

Declarar quién puede ver y hacer qué, en el motor y no solo en la aplicación. El control de acceso que vive únicamente en el código se salta con una consola y unas credenciales.

Resultados de aprendizaje

Al terminar podrás:

  1. Diseñar roles con privilegio mínimo y separación de funciones.
  2. Distinguir privilegios de objeto, de columna y de fila.
  3. Implementar seguridad por fila para multiinquilino.
  4. Reconocer los riesgos de SECURITY DEFINER y del rol PUBLIC.
  5. Auditar los privilegios efectivos de una base.

Fundamentos

Privilegio mínimo

Cada actor recibe solo lo que necesita. NIST SP 800-53 lo formaliza como control AC-6; OWASP lo sitúa como A01 (control de acceso roto), la categoría número uno de su Top 10.

La estructura habitual separa el rol (permisos) del usuario (identidad):

-- Roles por función, sin capacidad de conectarse
CREATE ROLE app_lectura   NOLOGIN;
CREATE ROLE app_escritura NOLOGIN;
CREATE ROLE app_migracion NOLOGIN;

GRANT USAGE ON SCHEMA public TO app_lectura, app_escritura, app_migracion;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_lectura;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_escritura;
GRANT ALL ON SCHEMA public TO app_migracion;

-- Los futuros objetos también, o el rol se queda obsoleto en la próxima migración
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO app_lectura;

-- Usuarios: identidades que se conectan
CREATE USER svc_api        LOGIN PASSWORD '...' IN ROLE app_escritura;
CREATE USER svc_informes   LOGIN PASSWORD '...' IN ROLE app_lectura;
CREATE USER svc_migraciones LOGIN PASSWORD '...' IN ROLE app_migracion;

Tres cuentas con tres alcances distintos. Si se filtran las credenciales de svc_informes, el atacante lee; no borra.

ALTER DEFAULT PRIVILEGES es imprescindible: sin él, cada tabla nueva creada por una migración queda invisible para el rol de lectura, y alguien acaba «arreglándolo» con un GRANT ALL.

Errores estructurales de PostgreSQL por defecto

-- Hasta PG 14, cualquiera podía crear objetos en el esquema public
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- Cualquiera puede conectarse a cualquier base
REVOKE CONNECT ON DATABASE mi_base FROM PUBLIC;

PUBLIC no es «los administradores»: es todo rol existente. Los CIS Benchmarks incluyen ambas revocaciones entre sus controles de configuración endurecida.

Seguridad por fila

Los privilegios de tabla son de todo o nada. Para multiinquilino hace falta filtrar por fila en el motor:

ALTER TABLE enrollments ENABLE ROW LEVEL SECURITY;
ALTER TABLE enrollments FORCE  ROW LEVEL SECURITY;   -- aplicar también al dueño

CREATE POLICY inquilino_aislado ON enrollments
  USING       (tenant_id = current_setting('app.tenant_id')::int)
  WITH CHECK  (tenant_id = current_setting('app.tenant_id')::int);

La aplicación fija la variable al tomar la conexión del agrupador:

with pool.connection() as conn:
    conn.execute("SELECT set_config('app.tenant_id', %s, true)", (tenant,))
    # `true` = local a la transacción: se limpia al terminar, y una conexión
    # reutilizada del agrupador no hereda el inquilino de la petición anterior.

El tercer argumento true es la diferencia entre un aislamiento correcto y una fuga entre inquilinos con agrupadores de conexiones.

Advertencia: la seguridad por fila no se aplica a superusuarios ni a roles con BYPASSRLS. La aplicación nunca debe conectarse como superusuario, aquí menos que nunca.

SECURITY DEFINER

Una función SECURITY DEFINER se ejecuta con los privilegios de quien la creó, no de quien la llama. Es útil y es la vía habitual de escalada de privilegios si se escribe mal:

CREATE FUNCTION resumen_curso(cid int) RETURNS TABLE(...) AS $$
  SELECT ... FROM enrollments WHERE course_id = cid;
$$ LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, public;

SET search_path es obligatorio: sin él, quien llama puede crear un objeto en un esquema propio que sombree al real y hacer que la función ejecute su código con privilegios ajenos.

flowchart TD
    U["Petición"] --> A["Autenticación<br/>quién eres"]
    A --> C["Conexión con usuario<br/>de mínimo privilegio"]
    C --> R["Roles: qué operaciones"]
    R --> O["Privilegios de objeto<br/>tabla, columna"]
    O --> RLS["Política por fila<br/>qué filas"]
    RLS --> D[("Datos")]
    RLS -.->|"no se aplica a"| SU["superusuario · BYPASSRLS"]
    O --> AUD["Registro de auditoría"]

Ejemplo trabajado

Plataforma multiinquilino: cada institución ve solo sus datos. Además, los docentes ven las notas de sus cursos y los estudiantes solo las propias.

ALTER TABLE enrollments ENABLE ROW LEVEL SECURITY;
ALTER TABLE enrollments FORCE  ROW LEVEL SECURITY;

-- Aislamiento entre instituciones: se aplica SIEMPRE, encima de todo lo demás
CREATE POLICY p_inquilino ON enrollments
  AS RESTRICTIVE
  USING (tenant_id = current_setting('app.tenant_id')::int);

-- Un estudiante ve sus propias inscripciones
CREATE POLICY p_estudiante ON enrollments
  FOR SELECT TO rol_estudiante
  USING (student_id = current_setting('app.user_id')::int);

-- Un docente ve las de los cursos que dicta
CREATE POLICY p_docente ON enrollments
  FOR SELECT TO rol_docente
  USING (EXISTS (SELECT 1 FROM teaching t
                 WHERE t.course_id = enrollments.course_id
                   AND t.teacher_id = current_setting('app.user_id')::int));

-- Y solo el docente puede calificar, solo sus cursos
CREATE POLICY p_docente_califica ON enrollments
  FOR UPDATE TO rol_docente
  USING      (EXISTS (SELECT 1 FROM teaching t WHERE t.course_id = enrollments.course_id
                        AND t.teacher_id = current_setting('app.user_id')::int))
  WITH CHECK (EXISTS (SELECT 1 FROM teaching t WHERE t.course_id = enrollments.course_id
                        AND t.teacher_id = current_setting('app.user_id')::int));

Cómo se combinan las políticas —esto es lo que se entiende mal—:

Por eso p_inquilino es RESTRICTIVE: garantiza que ninguna política permisiva futura pueda saltarse el aislamiento entre instituciones. Si fuera permisiva, añadir mañana una política de «administrador ve todo» abriría un agujero entre inquilinos.

Privilegios de columna para datos sensibles:

REVOKE SELECT ON students FROM app_lectura;
GRANT  SELECT (id, nombre, email) ON students TO app_lectura;   -- sin RUT ni dirección

Verificación, que es lo que convierte esto en ingeniería:

SET app.tenant_id = '1';
SET app.user_id   = '11';
SET ROLE rol_estudiante;

SELECT count(*) FROM enrollments;             -- solo las del estudiante 11
SELECT count(*) FROM enrollments WHERE student_id <> 11;   -- debe ser 0
INSERT INTO enrollments (tenant_id, student_id, course_id) VALUES (2, 11, 1);
-- ERROR: new row violates row-level security policy
RESET ROLE;

Estas comprobaciones deben ser pruebas automatizadas. Una política de seguridad sin prueba es una intención.

Auditoría de privilegios efectivos:

SELECT grantee, table_name, string_agg(privilege_type, ', ') AS privilegios
FROM information_schema.role_table_grants
WHERE table_schema = 'public'
GROUP BY grantee, table_name ORDER BY grantee, table_name;

SELECT rolname, rolsuper, rolbypassrls, rolcreatedb FROM pg_roles
WHERE rolsuper OR rolbypassrls;    -- debe ser una lista muy corta y conocida

Comparación

Necesidad Mecanismo
Servicio que solo lee Rol con SELECT
Ocultar columnas sensibles Privilegios de columna o vista
Aislar inquilinos Política restrictiva por fila
Un usuario ve solo lo suyo Política permisiva por fila
Operación privilegiada acotada SECURITY DEFINER con search_path fijo
Saber quién hizo qué Registro de auditoría

Errores frecuentes

  1. La aplicación se conecta como superusuario. Ignora toda la seguridad por fila.
  2. Un solo usuario para todo. Una filtración lo compromete todo.
  3. set_config sin local = true. El inquilino persiste en la conexión reutilizada del agrupador: fuga entre inquilinos.
  4. Política sin WITH CHECK. Se puede escribir en el ámbito de otro.
  5. Olvidar ALTER DEFAULT PRIVILEGES. Las tablas nuevas quedan mal permisionadas.
  6. PUBLIC con privilegios. Aplica a todos los roles.
  7. SECURITY DEFINER sin search_path. Escalada de privilegios.

De la clase a la operación

Las filtraciones de datos multiinquilino rara vez son un fallo del motor: son un WHERE tenant_id = ? que faltaba en una consulta nueva. La seguridad por fila hace que ese olvido sea imposible, porque el filtro deja de depender de que alguien lo escriba.

Reto de transferencia

  1. Diseña tres roles con privilegio mínimo para tu sistema.
  2. Implementa el aislamiento entre inquilinos con una política restrictiva.
  3. Escribe pruebas automatizadas que intenten leer y escribir fuera del ámbito.
  4. Audita los privilegios efectivos y los roles con BYPASSRLS.

Preguntas de evaluación

  1. ¿Por qué la política de inquilino debe ser restrictiva y no permisiva?
  2. Explica la fuga que produce set_config sin ámbito local en un agrupador.
  3. ¿Qué añade WITH CHECK que USING no cubre?
  4. Da un caso donde SECURITY DEFINER sea necesario y cómo lo asegurarías.

🌐 El mismo problema en cada motor

Caso: Cada cliente ve sus filas y solo las suyas

Tres notas de dos clientes en la misma tabla. La aplicación del cliente acme tiene que ver dos filas y no puede ver la tercera. La pregunta de la clase no es cómo filtrar —eso es un WHERE— sino dónde vive ese filtro: si está en cada consulta, basta que una lo olvide para filtrar los datos de otro cliente, y ese error no se detecta con pruebas funcionales porque la consulta devuelve resultados perfectamente plausibles.

Cada motor ofrece un sitio distinto donde poner la frontera: una política de fila que el servidor aplica siempre, una vista con permisos, un usuario por inquilino o nada en absoluto. El caso muestra la forma mínima —la vista— y la matriz dice hasta dónde llega cada uno.

Salida esperada, idéntica en todos los motores que lo resuelven:

estudiante nota
Ada 90
Bea 58

El contrato vive en motores.yaml y lo comprueba python scripts/verificar_equivalencia.py --clase 060: 5 de las 6 implementaciones se ejecutan de verdad y su resultado se compara con esa tabla; el resto se declara como material revisado, no ejecutado.

Motor ¿Resuelve el caso? Nivel de prueba Código Fuente
PostgreSQL servicio código doc oficial
SQLite núcleo código doc oficial
DuckDB núcleo código doc oficial
MySQL servicio código doc oficial
MongoDB servicio código doc oficial
Microsoft SQL Server declarado código doc oficial
Redis no doc oficial

Los que resuelven el caso

PostgreSQL · implementaciones/postgresql/consulta.sql

verificado — se ejecuta contra el motor real levantado con docker compose

-- motor: postgresql
-- doc: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
-- nota: aqui la frontera NO esta en la consulta: la aplica el servidor. El
--       SELECT de abajo no lleva ningun WHERE sobre el inquilino y aun asi
--       devuelve solo dos filas.
--
--       TRES trampas, y las tres se descubren tarde:
--       1) El SUPERUSUARIO se salta TODAS las politicas, siempre, y no hay
--          FORCE que lo impida. Como el usuario de la aplicacion en un entorno
--          de desarrollo suele ser superusuario, la proteccion parece no
--          funcionar. Por eso este guion crea un rol sin privilegios y cambia a
--          el antes de consultar: sin ese SET ROLE, esta consulta devuelve las
--          TRES filas.
--       2) FORCE ROW LEVEL SECURITY: sin el, el DUENO de la tabla tambien se
--          salta sus propias politicas.
--       3) La politica se evalua por fila: si consulta otras tablas, puede
--          impedir el uso de indices y costar mas que la propia consulta.

-- === preparacion ===
DROP TABLE IF EXISTS notas;

CREATE TABLE notas (
    inquilino  text NOT NULL,
    estudiante text NOT NULL,
    nota       integer NOT NULL,
    PRIMARY KEY (inquilino, estudiante)
);
INSERT INTO notas (inquilino, estudiante, nota) VALUES
    ('acme',   'Ada', 90),
    ('acme',   'Bea', 58),
    ('globex', 'Cid', 77);

ALTER TABLE notas ENABLE ROW LEVEL SECURITY;
ALTER TABLE notas FORCE ROW LEVEL SECURITY;

CREATE POLICY solo_mi_inquilino ON notas
    USING (inquilino = current_setting('app.inquilino', true));

-- El rol de la aplicacion: sin privilegios especiales, que es la unica forma de
-- que las politicas se le apliquen.
DO $$
BEGIN
    IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_inquilino') THEN
        CREATE ROLE app_inquilino NOLOGIN;
    END IF;
END;
$$;
-- Y los permisos minimos: ver el esquema y leer la tabla. Nada mas.
DO $$
BEGIN
    EXECUTE format('GRANT USAGE ON SCHEMA %I TO app_inquilino', current_schema());
END;
$$;
GRANT SELECT ON notas TO app_inquilino;

-- === consulta ===
-- La aplicacion fija su identidad al abrir la conexion. A partir de ahi,
-- ninguna consulta necesita —ni puede— saltarse el filtro.
SET app.inquilino = 'acme';
SET ROLE app_inquilino;

SELECT estudiante, nota FROM notas ORDER BY estudiante;

SQLite · implementaciones/sqlite/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: sqlite
-- doc: https://sqlite.org/lang_createview.html
-- nota: la vista es una CONVENCION, no un control de acceso: nada impide que el
--       mismo proceso consulte `notas` directamente. En SQLite, quien tiene el
--       archivo lo tiene todo.

-- === preparacion ===
CREATE TABLE notas (
    inquilino  TEXT NOT NULL,
    estudiante TEXT NOT NULL,
    nota       INTEGER NOT NULL,
    PRIMARY KEY (inquilino, estudiante)
);
INSERT INTO notas (inquilino, estudiante, nota) VALUES
    ('acme',   'Ada', 90),
    ('acme',   'Bea', 58),
    ('globex', 'Cid', 77);

-- La vista ES la frontera. La aplicacion consulta `mis_notas`, nunca `notas`,
-- y el filtro por inquilino deja de depender de que cada consulta se acuerde
-- de escribirlo. Basta UNA consulta que lo olvide para filtrar los datos de
-- otro cliente.
CREATE VIEW mis_notas AS
SELECT estudiante, nota
FROM notas
WHERE inquilino = 'acme';

-- === consulta ===
SELECT estudiante, nota FROM mis_notas ORDER BY estudiante;

DuckDB · implementaciones/duckdb/consulta.sql

verificado — se ejecuta en CI sin servicios

-- motor: duckdb
-- doc: https://duckdb.org/docs/stable/sql/statements/create_view
-- nota: la auditoria que precede a cualquier control de acceso:
--         SELECT inquilino, COUNT(*) FROM notas GROUP BY inquilino;
--         SELECT COUNT(*) FROM notas WHERE inquilino IS NULL OR inquilino = '';
--       La segunda busca el agujero clasico del esquema multiinquilino: filas
--       sin inquilino, que las politicas no saben de quien son.

-- === preparacion ===
CREATE TABLE notas (
    inquilino  VARCHAR NOT NULL,
    estudiante VARCHAR NOT NULL,
    nota       INTEGER NOT NULL,
    PRIMARY KEY (inquilino, estudiante)
);
INSERT INTO notas (inquilino, estudiante, nota) VALUES
    ('acme',   'Ada', 90),
    ('acme',   'Bea', 58),
    ('globex', 'Cid', 77);

-- La vista ES la frontera. La aplicacion consulta `mis_notas`, nunca `notas`,
-- y el filtro por inquilino deja de depender de que cada consulta se acuerde
-- de escribirlo. Basta UNA consulta que lo olvide para filtrar los datos de
-- otro cliente.
CREATE VIEW mis_notas AS
SELECT estudiante, nota
FROM notas
WHERE inquilino = 'acme';

-- === consulta ===
SELECT estudiante, nota FROM mis_notas ORDER BY estudiante;

MySQL · implementaciones/mysql/consulta.sql

verificado — se ejecuta contra el motor real levantado con docker compose

-- motor: mysql
-- doc: https://dev.mysql.com/doc/refman/8.4/en/create-view.html
-- nota: MySQL NO tiene seguridad a nivel de fila. Lo mas parecido es una vista
--       con SQL SECURITY DEFINER y permisos:
--         GRANT SELECT ON learning.mis_notas TO 'app_acme'@'%';
--         REVOKE ALL ON learning.notas FROM 'app_acme'@'%';
--       Asi la vista deja de ser una convencion y pasa a ser una frontera. El
--       limite: para cien inquilinos hacen falta cien vistas o cien bases.

-- === preparacion ===
DROP VIEW IF EXISTS mis_notas;
DROP TABLE IF EXISTS notas;

CREATE TABLE notas (
    inquilino  VARCHAR(50) NOT NULL,
    estudiante VARCHAR(50) NOT NULL,
    nota       INT NOT NULL,
    PRIMARY KEY (inquilino, estudiante)
);
INSERT INTO notas (inquilino, estudiante, nota) VALUES
    ('acme',   'Ada', 90),
    ('acme',   'Bea', 58),
    ('globex', 'Cid', 77);

-- La vista ES la frontera. La aplicacion consulta `mis_notas`, nunca `notas`,
-- y el filtro por inquilino deja de depender de que cada consulta se acuerde
-- de escribirlo. Basta UNA consulta que lo olvide para filtrar los datos de
-- otro cliente.
CREATE VIEW mis_notas AS
SELECT estudiante, nota
FROM notas
WHERE inquilino = 'acme';

-- === consulta ===
SELECT estudiante, nota FROM mis_notas ORDER BY estudiante;

MongoDB · implementaciones/mongodb/consulta.js

verificado — se ejecuta contra el motor real levantado con docker compose

// motor: mongodb
// doc: https://www.mongodb.com/docs/manual/core/views/
// nota: la vista de solo lectura con la tuberia filtrada, mas un rol que de
//       acceso a la vista y NO a la coleccion, consigue la misma frontera:
//         db.createRole({ role: "acme_lector",
//                         privileges: [{ resource: { db: "learning",
//                                                    collection: "mis_notas" },
//                                        actions: ["find"] }], roles: [] })
//       En la version Community no hay control por documento: es por coleccion.

// === preparacion ===
db.mis_notas.drop();
db.notas.drop();

db.notas.insertMany([
  { inquilino: "acme", estudiante: "Ada", nota: 90 },
  { inquilino: "acme", estudiante: "Bea", nota: 58 },
  { inquilino: "globex", estudiante: "Cid", nota: 77 },
]);

db.createView("mis_notas", "notas", [
  { $match: { inquilino: "acme" } },
  { $project: { _id: 0, estudiante: 1, nota: 1 } },
]);

// === consulta ===
db.mis_notas
  .find()
  .sort({ estudiante: 1 })
  .forEach((d) => print(d.estudiante + "|" + d.nota));

Microsoft SQL Server · implementaciones/sql-server/consulta.sql

declarado — se revisa a mano contra la documentación citada; la máquina no lo ejecuta

-- motor: sql-server
-- doc: https://learn.microsoft.com/sql/relational-databases/security/row-level-security
-- nota: implementacion declarada. SQL Server separa dos cosas que conviene no
--       confundir:
--         predicado de FILTRO   -> que filas se VEN
--         predicado de BLOQUEO  -> que filas se pueden ESCRIBIR
--       Sin el segundo, un usuario puede insertar filas de otro inquilino que
--       despues no podra ver: el dato se pierde de vista sin haberse borrado.

-- === preparacion ===
DROP SECURITY POLICY IF EXISTS dbo.politica_inquilino;
DROP FUNCTION IF EXISTS dbo.fn_inquilino;
DROP TABLE IF EXISTS dbo.notas;

CREATE TABLE dbo.notas (
    inquilino  NVARCHAR(50) NOT NULL,
    estudiante NVARCHAR(50) NOT NULL,
    nota       INT NOT NULL,
    CONSTRAINT pk_notas PRIMARY KEY (inquilino, estudiante)
);
INSERT INTO dbo.notas (inquilino, estudiante, nota) VALUES
    (N'acme', N'Ada', 90), (N'acme', N'Bea', 58), (N'globex', N'Cid', 77);
GO

-- SCHEMABINDING y una funcion simple: se ejecuta POR FILA.
CREATE FUNCTION dbo.fn_inquilino(@inquilino AS NVARCHAR(50))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS visible
       WHERE @inquilino = CAST(SESSION_CONTEXT(N'inquilino') AS NVARCHAR(50));
GO

CREATE SECURITY POLICY dbo.politica_inquilino
ADD FILTER PREDICATE dbo.fn_inquilino(inquilino) ON dbo.notas,
ADD BLOCK PREDICATE dbo.fn_inquilino(inquilino) ON dbo.notas AFTER INSERT
WITH (STATE = ON);
GO

-- === consulta ===
EXEC sp_set_session_context @key = N'inquilino', @value = N'acme';

SELECT estudiante, nota FROM dbo.notas ORDER BY estudiante;

Los que no resuelven este caso — y qué se hace en su lugar

Descartar un motor con un argumento es tan formativo como usarlo. Ninguna de estas filas dice que el motor sea peor: dice que este problema no es el suyo.

Motor Por qué no Qué se hace en su lugar Fuente
Redis El control de acceso llega hasta el patrón de clave: las ACL permiten restringir a ~acme:*, pero no hay forma de filtrar dentro de un valor. Si los datos de dos inquilinos comparten una estructura, no hay frontera posible. Separar por prefijo de clave desde el diseño (acme:notas:...) y usar ACL por usuario sobre ese patrón: la frontera se consigue por nomenclatura, y hay que sostenerla en cada escritura. doc

Laboratorio

python scripts/validate_repository.py
python labs/03-transactions/run_transactions_lab.py

Guarda como evidencia la salida completa, la versión del motor y la semilla o los parámetros usados. Una captura sin comando no es evidencia: no se puede repetir.

Evaluación

Criterio Peso Qué se comprueba
Comprensión conceptual 25 % Explica el mecanismo, no solo el resultado
Ejecución reproducible 25 % Otra persona obtiene lo mismo con las instrucciones dadas
Interpretación basada en evidencia 25 % Cada conclusión se apoya en una salida o una medición
Límites y riesgos declarados 25 % Dice qué no demuestra el ejercicio y qué faltaría en producción

La clase se da por superada cuando la respuesta explica el mecanismo, muestra la salida que la respalda y declara al menos un límite del ejercicio.

Fuentes de esta clase

Todo lo afirmado arriba procede de estas obras. Los identificadores viven en catalog/sources.json y el estado de los enlaces se comprueba con python scripts/check_external_links.py.


Programa · Parte 11 · ← Anterior · Siguiente →