En un artículo anterior —que les recomiendo leer antes de continuar— les mostré lo simple y rápido que es ocultar datos usando Virtual Private Database (VPD), y además sin tener que recurrir a cambios en las aplicaciones, pero recientemente atendí a la extraordinaria presentación de Karen Cannell y Jim Czuprynski: RAS before RAG: Real Application Security Fundamentals for Gen AI Apps, que puedes ver y descargar aquí, en el que presentan un problema similar pero que es resuelto empleando algo nuevo: Real Application Security (RAS).
¿Pero, qué es RAS?¿Será una mejor forma de lograr el objetivo? ¿Será que VPD ya es historia? Pues sigue leyendo para averiguarlo.
Real Application Security
La documentación oficial nos lo presenta así:
Oracle Database Real Application Security es un modelo de autorización de bases de datos que:
- Admite políticas de seguridad declarativas.
- Permite una seguridad integral para aplicaciones de varios niveles.
- Proporciona una solución integrada para proteger los recursos de bases de datos y aplicaciones.
- Mejora la arquitectura de seguridad de Oracle Database para satisfacer las demandas actuales y emergentes de las aplicaciones desarrolladas para Internet.
RAS Administrator's and Developer's Guide
Esta es la teoría, pero ¿cómo será en la práctica? Pues nada mejor para saberlo que tratar de implementarlo, vayamos a ello.
La solución al estilo RAS
Si no lo has hecho aún, dale una leída al artículo anterior, para que así te familiarices con el problema que vamos a resolver.
Tomemos en cuenta que inicialmente podemos ver las columnas sensibles sin restricción alguna.
Ahora sí, como primer paso debemos crear un Security Class, que viene a ser un marco para un conjunto de privilegios de aplicación.
En nuestro caso se llamará SH_PRIVILEGES, y el privilegio de aplicación que definiremos es: VIEW_SENSITIVE_COLUMNS.
BEGIN
xs_security_class.create_security_class(
name => 'SH_PRIVILEGES',
priv_list => xs$privilege_list(xs$privilege(name => 'VIEW_SENSITIVE_COLUMNS')),
parent_list => xs$name_list('SYS.DML'),
description => 'Security Class to protect SH tables'
);
END;
/
Ahora crearemos una Access Control List (ACL), que viene a ser una lista de Access Control Entries (ACE), que permiten o deniegan privilegios de aplicación a uno o más usuarios o roles (PRINCIPAL).
Nuestra primera ACL servirá para permitir el acceso a las columnas sensibles, la llamaremos SH_UNRESTRICTED_ACL, le asignaremos el privilegio condicional que definimos en el paso previo: VIEW_SENSITIVE_COLUMNS, y como necesitamos que aplique a todos los usuarios de la base de datos, lo asociaremos al rol: PUBLIC.
DECLARE
aces xs$ace_list := xs$ace_list();
BEGIN
aces.extend(1);
aces(1) := xs$ace_type(
privilege_list => xs$name_list('SELECT', 'VIEW_SENSITIVE_COLUMNS'),
principal_name => 'PUBLIC',
principal_type => xs_acl.ptype_db
);
xs_acl.create_acl(
name => 'SH_UNRESTRICTED_ACL',
ace_list => aces,
sec_class => 'SH_PRIVILEGES',
description => 'Conditional viewing'
);
END;
/
A continuación crearemos una segunda ACL, que servirá para prohibir el acceso a las columnas sensibles, la llamaremos SH_RESTRICTED_ACL, y como necesitamos que aplique a todos los usuarios de la base de datos, la asociaremos al rol: PUBLIC.
DECLARE
aces xs$ace_list := xs$ace_list();
BEGIN
aces.extend(1);
aces(1) := xs$ace_type(
privilege_list => xs$name_list('SELECT'),
principal_name => 'PUBLIC',
principal_type => xs_acl.ptype_db
);
xs_acl.create_acl(
name => 'SH_RESTRICTED_ACL',
ace_list => aces,
sec_class => 'SH_PRIVILEGES',
description => 'Limited viewing'
);
END;
/
Como siguiente paso debemos crear una Data Security Policy, que define quién puede hacer qué (ACL) y sobre qué filas (realms) o columnas (cols). Le llamaremos SH_DS.
Vamos a considerar dos grupos de filas: el primero abarca todas las filas para las que las columnas sensibles podrán ser vistas condicionalmente (realm 1, con ACL SH_UNRESTRICTED_SESSION), y el segundo incluye todo el universo de filas que tienen prohibido ver las columnas sensibles (realm 2, con ACL SH_RESTRICTED_SESSION).
Definimos las columnas que serán visibles de forma condicional (QUANTITY_SOLD, AMOUNT_SOLD, TIME_ID), de acuerdo a la disponibilidad o no del privilegio VIEW_SENSITIVE_COLUMNS.
DECLARE
realms xs$realm_constraint_list := xs$realm_constraint_list();
cols xs$column_constraint_list := xs$column_constraint_list();
BEGIN
realms.extend(2);
cols.extend(1);
realms(1) := xs$realm_constraint_type(
realm => 'prod_id NOT IN ( SELECT prod_id FROM sh.products WHERE prod_category = ''Golf'' )',
acl_list => xs$name_list('SH_UNRESTRICTED_ACL')
);
realms(2) := xs$realm_constraint_type(
realm => '1=1',
acl_list => xs$name_list('SH_RESTRICTED_ACL')
);
cols(1) := xs$column_constraint_type(
column_list => xs$list('QUANTITY_SOLD', 'AMOUNT_SOLD', 'TIME_ID'),
privilege => 'VIEW_SENSITIVE_COLUMNS'
);
xs_data_security.create_policy(
name => 'SH_DS',
realm_constraint_list => realms,
column_constraint_list => cols,
description => 'Controls access to sensitive columns'
);
END;
/
BEGIN
xs_data_security.apply_object_policy(
policy => 'SH_DS',
schema => 'SH',
object => 'SALES',
owner_bypass => TRUE,
statement_types => 'SELECT'
);
END;
/
No hay más que hacer, ya tenemos todo listo, así que procedamos a repetir la consulta para ver qué pasa.
¡Excelente! Ahora ya no es posible ver los datos de las columnas sensibles, pero solo para las filas que tienen un producto de la categoría Golf, para todos los demás casos son plenamente visibles.
RAS vs VPD: flexibilidad
Cuando originalmente usamos VPD y ahora que lo hacemos con RAS, el resultado es el mismo: absolutamente todos los usuarios están impedidos de ver las columnas sensibles para el universo de filas definido, pero ¿qué tal si necesitamos que haya usuarios para los cuales esta regla no se aplique?, es decir: que puedan ver los contenidos de estas columnas siempre.
Con VPD tendríamos que reescribir la función para que la cadena que retorna se vaya ajustando. Por ejemplo, si deseamos que el usuario SUPER_USER escape a las reglas, el código sería:
CREATE OR REPLACE FUNCTION sh.vpd_condition (
object_schema IN VARCHAR2,
object_name IN VARCHAR2)
RETURN VARCHAR2 AS
condition VARCHAR2 (100);
BEGIN
IF sys_context('USERENV','SESSION_USER') = 'SUPER_USER' THEN
condition := NULL;
ELSE
condition := 'prod_id NOT IN ( SELECT prod_id FROM sh.products WHERE prod_category = ''Golf'' )';
END IF;
RETURN (condition);
END vpd_condition;
/
Si bien lo hemos resuelto, esto es poco práctico y elegante, ¿qué pasa si deseamos modificar la lista de usuarios que escapan de las reglas? ¿no sería mejor considerar roles en lugar de listar toda una serie de usuarios que puede ir cambiando en el tiempo?
Es aquí donde podemos apreciar la flexibilidad que nos da RAS, ya que es cuestión de crear un nuevo ACL y añadirlo al Data Security Policy.
Por ejemplo, si deseamos que todos los usuarios que tienen asignado el rol SH_FULL_ROLE puedan ver las columnas sensibles sin restricción alguna, crearemos un ACL al que llamaremos SH_FULL_ACL, y le asignaremos el privilegio condicional VIEW_SENSITIVE_COLUMNS.
DECLARE
aces xs$ace_list := xs$ace_list();
BEGIN
aces.extend(1);
aces(1) := xs$ace_type(
privilege_list => xs$name_list('SELECT', 'VIEW_SENSITIVE_COLUMNS'),
principal_name => 'SH_FULL_ROLE',
principal_type => xs_acl.ptype_db
);
xs_acl.create_acl(
name => 'SH_FULL_ACL',
ace_list => aces,
sec_class => 'SH.SH_PRIVILEGES',
description => 'Limited viewing'
);
END;
/
Como siguiente paso vamos a añadir la ACL al Data Security Policy, para lo cual vamos a considerar un grupo de filas (realm) que abarca todas las filas existentes.
DECLARE
realm_cons xs$realm_constraint_type;
BEGIN
realm_cons :=
xs$realm_constraint_type(
realm => '1=1',
acl_list => xs$name_list('SH.SH_FULL_ACL')
);
xs_data_security.append_realm_constraints(
policy => 'SH.SH_DS',
realm_constraint => realm_cons
);
END;
/
Ahora repetimos la consulta, pero con un usuario que tiene asignado el rol SH_FULL_ROLE, y comprobamos que tiene acceso irrestricto a las columnas sensibles.
Conclusiones
Hemos comprobado que es cierto aquello de que «hay más de una forma de despellejar un gato», pero si bien VPD nos ayudó a resolver nuestro problema inicial muy rápidamente, RAS es una solución mucho más flexible. Y eso que en este artículo solo hemos arañado la superficie, hay todo un mundo por explorar y aprovechar.
Si bien hay que leer una extensa documentación y no hay muchos artículos con ejemplos que nos ayuden a comprenderlo mejor, el esfuerzo bien vale la pena. ¡Adiós VPD, bienvenido RAS!
Finalmente agradezco la ayuda de Jim Czuprynski y Thomas Minne para absolver mis dudas iniciales, y sin la cual esta tarea de aprendizaje hubiera tomado mucho más tiempo.