Implementando o controle de acesso Fine-Grained para o VPD

Por Tércio Silva Costa Oracle Associate
Publicado em Maio 2018

Revisado por Lorenzo Mota


Antes de mais nada, a sigla VPD significa Virtual Private Database. Essa é uma feature do Oracle com foco em segurança, em que altera em tempo real a query que um certo usuário está realizando.

Imagine que alguém tem acesso a tabela employees do sample schema HR, mas queremos que ele tenha acesso apenas ao departamento dele por exemplo. Se ele tem grant de SELECT na tabela, ele poderá ver tudo. Algo que pode remediar isto, seria criar uma view para cada departamento, e assim dar grant nessa view para os usuários de cada departamento. Bem trabalhoso não é verdade?

Com a ajuda dessa feature, esse trabalho poderá ser bem melhor organizado e totalmente transparente para o usuário que opera diretamente na tabela sem ser por uma view. Ao aplicar uma política VPD é importante saber que a query será reestruturada, ou seja, irá impactar na performance, visto que irá adicionar mais um predicado à query e assim o caminho que o Oracle irá escolher para o access e filter poderá se alterar, tendo implicações na escolha de index e etc. Isso acontece por é como se coloca-se mais uma cláusula no where na query, essa será fixa sempre que a política estiver ativa. Isso é um clássico exemplo de Row Level Security (RLS), assim a porção de dados disponível para cada usuário é um aparente Virtual Database.

Para o exemplo da tabela employees, podemos criar uma política na coluna departament_id associada a tabela employees e essa política será associada ao usuário do banco. É como se alguém que fizesse a seguinte query da Listagem 1 e ela seria automaticamente reescrita para a Listagem 2, lembrando que o número de departamento é colocado na política de acesso.

Listagem 1. SQL Original.

SELECT *
FROM   hr.employees;


Listagem 2. SQL modificado com a política de acesso.

SELECT *
FROM   hr.employees
WHERE  departament_id = 10;


Para que isto seja possível, devemos criar algo no Oracle chamado application context, que nada mais é que um conjunto de variáveis disponíveis na sessão, uma vez atribuído valores para essas variáveis, elas se manterão as mesmas durante toda a sessão. Um exemplo de um context está na listagem 3.


Listagem 3. Retornando o valor da variável de context DB_NAME

SELECT sys_context('USERENV','DB_NAME')
FROM   dual;


A função sys_context recebeu dois argumentos do tipo VARCHAR. O primeiro é o namespace e o Oracle já provê o namespace USERENV que descreve a sessão atual. Não iremos nos aprofundar nessa função, mas o segundo argumento é a variável que queremos retornar o valor, no caso foi o DB_NAME e no meu caso retornou o service name orcl. Outros exemplos poderia ser CURRENT_USER,CURRENT_USERID e etc.

Ao criar esse context com a package DBMS_SESSION.SET_CONTEXT, deveremos informar uma package que irá ser utilizada para associar os valores as variáveis, no momento da criação do context o Oracle não verifica se o package que será utilizado existe, mas logo após deveremos criar com o mesmo nome utilizado no momento da criação do context. Um passo após esse seria automatizar através de uma trigger de logon para aplicar o context a cada sessão que é iniciada. Logo após iremos ver como criar a política de acesso e como associar ela da maneira desejada.

Vamos primeiro criar um usuário em que irá residir tudo relativo ao nosso context, além de criar o mesmo. Veja na Listagem 4.


Listagem 4. Criando o context

CREATE USER security_context IDENTIFIED BY oraclepress;

GRANT CREATE ANY CONTEXT TO security_context;

GRANT EXECUTE ON DBMS_RLS TO PUBLIC;

GRANT CONNECT, RESOURCE TO security_context;

CONN security_context/oraclepress

CREATE CONTEXT teste_context USING security_context.pck_contexts;

CREATE OR REPLACE PACKAGE security_context.pck_contexts AS
  PROCEDURE set_context(p_val NUMBER);
END;

CREATE OR REPLACE PACKAGE BODY security_context.pck_contexts AS
  PROCEDURE set_context(p_val NUMBER) IS  
  BEGIN
    DBMS_SESSION.SET_CONTEXT(NAMESPACE => 'TESTE_CONTEXT', ATTRIBUTE => 'DEPARTAMENT', VALUE => p_val);
  END;
END;


Na Listagem 4 acima, criamos o usuário security_context que servirá de owner para os nossos objetos, como o context e a package. Foi dada permissão de CREATE ANY CONTEXT para este usuário além de executar DBMS_RLS para public, ou seja, para todos os usuários. Após istom conectado com o usuário security_context criamos um context de teste para o nosso exemplo, com o nome teste_context que utilizará a package pck_contexts que ainda não existe, mas logo em seguida foi criada, com apenas uma procedure, set_context, para aplicar o context. Foi utilizado o namespace TEXT_CONTEXT com o atributo DEPARTMENT no qual iremos atribuir o ID do departamento do funcionário mais a frente. Antes de prosseguir, vamos testar o context recém criado, observe a Listagem 5.


Listagem 5. Testando o context

SELECT sys_context('TESTE_CONTEXT','DEPARTAMENT')
FROM   dual;


O resultado da Listagem 5 é NULL! Isto mesmo, null. Isso ocorre porque ainda não foi associado nenhum valor ao context DEPARTMENT. Vamos colocar um valor e rodar novamente a query, isto está na Listagem 6.


Listagem 6. Colocando valor no context e testando o mesmo

BEGIN
  security_context.pck_contexts.set_context(10);
END;

SELECT sys_context('TESTE_CONTEXT','DEPARTAMENT')
FROM   dual;


Agora sim, o resultado da  Listagem 6 não é mais NULL, e sim o valor 10 é retornado. Isso será utilizado via trigger para associar o valor do department_id do funcionário. Veja um exemplo de trigger deste tipo na Listagem 7.


Listagem 7. Trigger para associar um valor ao context.

CREATE OR REPLACE TRIGGER security_context.set_sec_context
AFTER LOGON ON DATABASE
BEGIN
  security_context.pck_contexts.set_context(10);
END;


Claro que para a trigger ser funcional, teria que ser algo dinâmico e não estático com um valor único como é feito na Listagem 7. Mas aqui serviu apenas de exemplo que o contexto poderá ser aplicado assim que o usuário logar na base.

Esse context será utilizado para criar o predicado que será colocada na query, será uma cláusula WHERE como conhecemos muito bem. Faremos isto através de uma function de uma package, e essa function por sua vez irá usar o context. Veja isto na Listagem 8.


Listagem 8. Package para criar o predicado do WHERE

CREATE OR REPLACE PACKAGE security_context.pck_predicates AS
  FUNCTION get_predicate(p_owner VARCHAR2, p_table VARCHAR2) RETURN VARCHAR2;
END;


CREATE OR REPLACE PACKAGE BODY security_context.pck_predicates AS
  FUNCTION get_predicate(p_owner VARCHAR2, p_table VARCHAR2) RETURN VARCHAR2 IS  
  BEGIN
    RETURN 'DEPARTMENT_ID = SYS_CONTEXT(''TESTE_CONTEXT'',''DEPARTAMENT'')';
  END;
END;


Percebam o uso dessa package de exemplo, estamos com uma função que irá retornar uma string que irá restringir a query com a cláusula WHERE. Para usar isto em um VPD, temos que usar como argumento da função o nome do onwer, usuário, da tabela além do nome da tabela. Essa function da package pck_predicates será utilizada na hora de adicionar uma política de acesso utilizando o package DBMS_RLS. Esse último passo está descrito na Listagem 9.


Listagem 9. Criando uma política de acesso.

BEGIN

  DBMS_RLS.ADD_POLICY(object_schema   => 'HR',
                      object_name     => 'EMPLOYEES',
                      policy_name     => 'CONTEXT_EMP_DEP',
                      function_schema => 'SECURITY_CONTEXT',
                      policy_function => 'PCK_PREDICATES.GET_PREDICATE',
                      statement_types => 'SELECT');

END;


Ao executarmos um simples SELECT na tabela HR.EMPLOYEES antes disso tudo o resultado iria conter 107 linhas que seria o resultado do FULL TABLE SCAN, após isso tudo o resultado será somente uma linha, pois apenas um único funcionário está associado ao departamento com código 10. Veja mais detalhes na Listagem 10.


Listagem 10. Resultado da política de acesso.

SELECT *
FROM   hr.employees;




Na Listagem 10 foi executado um simples SELECT * FROM hr.employees mas que retornou apenas uma linha como resultado e podemos perceber o que aconteceu com um print do explain plan. Podem ver que foi colocado um predicado, que foi retorna da função criada anteriormente. E assim ele utilizou um índice com base nisto.



Tércio Costa Formado em Ciências da Computação pela UFPB com experiência em Servidores Windows Server e Linux e banco de dados Oracle desde 2008 juntamente com os seus serviços. Desenvolvimento de Sistemas em Java SE com banco de dados Oracle e MySQL. Certificado Oracle Certified SQL Expert, mantendo o blog https://oraclepress.wordpress.com/ reconhecido pela OTN e articulista no portal http://www.profissionaloracle.com.br/gpo

Este artigo foi revisto pela equipe de produtos Oracle e está em conformidade com as normas e práticas para o uso de produtos Oracle.