5.9. Políticas de segurança de linha #

Além do sistema de privilégios padrão do SQL, disponível por meio do comando GRANT, as tabelas podem ter políticas de segurança de linha que restringem, por usuário, quais linhas podem ser retornadas por consultas comuns, ou inseridas, atualizadas ou excluídas por comandos de modificação de dados. Esse recurso também é conhecido como Segurança no Nível de Linha. Por padrão, as tabelas não possuem políticas, de modo que, se um usuário tiver privilégios de acesso a uma tabela conforme o sistema de privilégios do SQL, todas as linhas da tabela estarão igualmente disponíveis para consulta ou atualização.

Quando a segurança da tabela está ativada no nível de linha (pelo comando ALTER TABLE ... ENABLE ROW LEVEL SECURITY), todo acesso usual à tabela para selecionar ou modificar linhas deve ser permitido por uma política de segurança de linha. (No entanto, normalmente o dono da tabela não está sujeito a políticas de segurança de linha.) Se nenhuma política existir para a tabela, a política de negação padrão será usada, significando que nenhuma linha estará visível, ou poderá ser modificada. As operações que se aplicam a toda a tabela, como TRUNCATE e REFERENCES, não estão sujeitas à segurança de linha.

As políticas de segurança de linha podem ser específicas para comandos, funções de banco de dados (roles), ou ambos. Uma política pode ser especificada para ser aplicada a todos os comandos (ALL), ou a SELECT, INSERT, UPDATE ou DELETE. Podem ser atribuídas várias funções de banco de dados a uma determinada política, e as regras normais de associação e herança são aplicadas.

Para especificar quais linhas são visíveis ou modificáveis de acordo com uma política, é necessária uma expressão que retorne um resultado booleano. Esta expressão será avaliada para cada linha antes de quaisquer condições ou funções provenientes da consulta do usuário. (As únicas exceções a esta regra são as funções à prova de vazamento, que garantem que não vão vazar informações; o otimizador pode optar por aplicar essas funções antes da verificação de segurança de linha). Linhas para as quais a expressão não retorna verdade não são processadas. Podem ser especificadas expressões separadas para proporcionar controle independente sobre as linhas que podem ser vistas e as linhas que podem ser modificadas. As expressões de política são executadas como parte da consulta e com os privilégios do usuário que executa a consulta, embora as funções do definidor de segurança possam ser usadas para acessar dados não disponíveis ao usuário que faz a chamada.

Superusuários e funções de banco de dados (roles) com o atributo BYPASSRLS sempre ignoram o sistema de segurança de linha ao acessar uma tabela. Normalmente a segurança de linha também é ignorada para os donos das tabelas, embora o dono da tabela possa optar por estar sujeito à segurança de linha usando ALTER TABLE ... FORCE ROW LEVEL SECURITY.

Ativar e desativar a segurança de linha, assim como adicionar políticas a uma tabela, é sempre um privilégio apenas do dono da tabela.

As políticas são criadas usando o comando CREATE POLICY, modificadas usando o comando ALTER POLICY, e excluídas usando o comando DROP POLICY. Para ativar e desativar a segurança de linha para uma determinada tabela é usado o comando ALTER TABLE.

Cada política tem seu nome, podendo ser definidas várias políticas para uma tabela. Como as políticas são específicas da tabela, cada política da tabela deve ter um nome único. Tabelas diferentes podem ter políticas com o mesmo nome.

Quando são aplicadas várias políticas a uma mesma consulta, elas são combinadas usando OR (para políticas permissivas, que são o padrão) ou usando AND (para políticas restritivas). Isso é semelhante à regra de que uma determinada função tem os privilégios de todas as roles das quais é membro. Políticas permissivas versus restritivas são discutidas mais adiante.

Como um exemplo simples, aqui está como criar uma política na tabela contas para permitir que apenas os membros da função de banco de dados (role) gerentes acessem as linhas, e somente as linhas de suas contas:

CREATE TABLE contas (gerente text, empresa text, email_contato text);

ALTER TABLE contas ENABLE ROW LEVEL SECURITY;

-- A função de banco de dados gerentes precisa estar criada
-- antes de executar o comando abaixo

CREATE POLICY gerentes_das_contas ON contas TO gerentes
    USING (gerente = current_user);

A política acima produz, implicitamente, uma cláusula WITH CHECK idêntica à sua cláusula USING, de modo que a restrição se aplica tanto às linhas selecionadas por um comando (assim, um gerente não pode usar SELECT, UPDATE ou DELETE nas linhas existentes pertencentes a outro gerente) quanto às linhas modificadas por um comando (assim, as linhas pertencentes a outro gerente não podem ser criadas via INSERT ou UPDATE).

Se nenhuma função de banco de dados (role) for especificada, ou o nome especial de usuário PUBLIC for usado, então a política será aplicada a todos os usuários do sistema. Para permitir que todos os usuários acessem apenas a sua própria linha na tabela usuarios, pode ser usada uma política simples:

CREATE POLICY politica_de_usuario ON usuarios
    USING (nome_do_usuario = current_user);

Isso funciona de forma semelhante ao exemplo anterior.

Para usar políticas diferentes para as linhas adicionadas à tabela e para as linhas visíveis, podem ser combinadas múltiplas políticas. O par de políticas abaixo permite que todos os usuários visualizem todas as linhas da tabela usuarios, mas só permite modificar as suas próprias linhas:

CREATE POLICY politica_seleciona_usuario ON usuarios
    FOR SELECT
    USING (true);
CREATE POLICY politica_modifica_usuario ON usuarios
    USING (nome_do_usuario = current_user);

Em um comando SELECT essas duas políticas são combinadas usando OR, com o efeito final de que todas as linhas podem ser selecionadas. Em outros tipos de comando, apenas a segunda política se aplica, para que os efeitos sejam os mesmos de antes.

A segurança de linha também pode ser desativada pelo comando ALTER TABLE. A desativação da segurança de linha não remove nenhuma política definida na tabela; elas são simplesmente ignoradas. Depois disso, todas as linhas da tabela se tornam visíveis e modificáveis, sujeitas ao sistema de privilégios padrão do SQL.

Abaixo está um exemplo maior de como as políticas podem ser usadas em ambientes de produção. A tabela passwd emula um arquivo de senha do Unix:

-- Exemplo simples baseado no arquivo passwd
CREATE TABLE passwd (
  user_name             text UNIQUE NOT NULL,
  pwhash                text,
  uid                   int  PRIMARY KEY,
  gid                   int  NOT NULL,
  real_name             text NOT NULL,
  home_phone            text,
  extra                 text,
  home_dir              text NOT NULL,
  shell                 text NOT NULL
);

CREATE ROLE admin;  -- Administrador
CREATE ROLE bob;    -- Usuário comum
CREATE ROLE alice;  -- Usuário comum

-- Inserir os dados na tabela
INSERT INTO passwd VALUES
  ('admin','xxx',0,0,'Admin','111-222-3333',null,'/root','/bin/dash');
INSERT INTO passwd VALUES
  ('bob','xxx',1,1,'Bob','123-456-7890',null,'/home/bob','/bin/zsh');
INSERT INTO passwd VALUES
  ('alice','xxx',2,1,'Alice','098-765-4321',null,'/home/alice','/bin/zsh');

-- Certifique-se de ativar a segurança ao nível de linha na tabela
ALTER TABLE passwd ENABLE ROW LEVEL SECURITY;

-- Criar as políticas
-- O administrador pode ver todas as linhas e adicionar qualquer linha
CREATE POLICY admin_tudo ON passwd TO admin USING (true) WITH CHECK (true);
-- Os usuários comuns podem visualizar todas as linhas
CREATE POLICY todas_vistas ON passwd FOR SELECT USING (true);
-- Os usuários comuns podem atualizar seus próprios registros, mas
-- estão limitados com relação a quais shells podem definir
CREATE POLICY modif_usuario ON passwd FOR UPDATE
  USING (current_user = user_name)
  WITH CHECK (
    current_user = user_name AND
    shell IN ('/bin/bash','/bin/sh','/bin/dash','/bin/zsh','/bin/tcsh')
  );

-- Conceder todos os privilégios para o administrador
GRANT SELECT, INSERT, UPDATE, DELETE ON passwd TO admin;
-- Os usuários comuns só têm acesso às colunas públicas
GRANT SELECT
  (user_name, uid, gid, real_name, home_phone, extra, home_dir, shell)
  ON passwd TO public;
-- Permitir os usuário comuns atualizarem algumas colunas
GRANT UPDATE
  (pwhash, real_name, home_phone, extra, shell)
  ON passwd TO public;

Como acontece com qualquer configuração de segurança, é importante testar para garantir que o sistema esteja se comportando conforme o desejado. Usando o exemplo acima, o material visto a seguir mostra que o sistema de permissão está funcionando corretamente.

-- O Administrador pode ver todos os campos e linhas da tabela
postgres=> set role admin;
SET
postgres=> table passwd;

 user_name | pwhash | uid | gid | real_name |  home_phone  | extra |  home_dir   |   shell
-----------+--------+-----+-----+-----------+--------------+-------+-------------+-----------
 admin     | xxx    |   0 |   0 | Admin     | 111-222-3333 |       | /root       | /bin/dash
 bob       | xxx    |   1 |   1 | Bob       | 123-456-7890 |       | /home/bob   | /bin/zsh
 alice     | xxx    |   2 |   1 | Alice     | 098-765-4321 |       | /home/alice | /bin/zsh
(3 linhas)

-- Verificar o que a Alice pode fazer
postgres=> set role alice;
SET
postgres=> table passwd;
ERRO:  permissão negada para tabela passwd
postgres=> select user_name,real_name,home_phone,extra,home_dir,shell from passwd;

 user_name | real_name |  home_phone  | extra |  home_dir   |   shell
-----------+-----------+--------------+-------+-------------+-----------
 admin     | Admin     | 111-222-3333 |       | /root       | /bin/dash
 bob       | Bob       | 123-456-7890 |       | /home/bob   | /bin/zsh
 alice     | Alice     | 098-765-4321 |       | /home/alice | /bin/zsh
(3 linhas)

postgres=> update passwd set user_name = 'joe';
ERRO:  permissão negada para tabela passwd
-- Alice tem permissão para alterar seu próprio nome verdadeiro, mas de nenhum outro
postgres=> update passwd set real_name = 'Alice Doe';
UPDATE 1
postgres=> update passwd set real_name = 'John Doe' where user_name = 'admin';
UPDATE 0
postgres=> update passwd set shell = '/bin/xx';
ERRO:  a nova linha viola a política de segurança no nível de linha da tabela "passwd"
postgres=> delete from passwd;
ERRO:  permissão negada para tabela passwd
postgres=> insert into passwd (user_name) values ('xxx');
ERRO:  permissão negada para tabela passwd
-- Alice pode alterar sua própria senha;
-- A segurança no nível de linha (RLS)
-- impede silenciosamente a atualização de outras linhas
postgres=> update passwd set pwhash = 'abc';
UPDATE 1

Todas as políticas criadas até agora foram políticas permissivas, significando que, quando várias políticas são aplicadas, elas são combinadas usando o operador booleano OR. Embora as políticas permissivas possam ser construídas para permitir apenas o acesso às linhas nos casos pretendidos, pode ser mais simples combinar políticas permissivas com políticas restritivas (nas quais os registros devem passar, e são combinadas usando o operador booleano AND). Baseado no exemplo acima, adicionamos uma política restritiva para exigir que o administrador esteja conectado por meio de um soquete Unix local para acessar os registros da tabela passwd:

CREATE POLICY somente_admin_local ON passwd AS RESTRICTIVE TO admin
    USING (pg_catalog.inet_client_addr() IS NULL);

Abaixo podemos ver que o administrador conectado por meio de TCP/IP não tem acesso a nenhum registro, devido à política restritiva:

=> SELECT current_user;

 current_user
--------------
 admin
(1 linha)

=> select inet_client_addr();

 inet_client_addr
------------------
 127.0.0.1
(1 linha)

=> TABLE passwd;

 user_name | pwhash | uid | gid | real_name | home_phone | extra | home_dir | shell
-----------+--------+-----+-----+-----------+------------+-------+----------+-------
(0 linhas)

=> UPDATE passwd set pwhash = NULL;
UPDATE 0

As verificações de integridade referencial, como restrições de unicidade ou de chave primária e referências de chave estrangeira, sempre ignoram a segurança de linha para garantir que a integridade dos dados seja mantida. Deve-se tomar cuidado ao desenvolver esquemas e políticas no nível de linha para evitar vazamentos de informações de canal encoberto [35] por meio das verificações de integridade referencial.

Em alguns contextos é importante ter certeza de que a segurança de linha não está sendo aplicada. Por exemplo, ao se fazer uma cópia de segurança pode ser desastroso se a segurança de linha fizer silenciosamente com que algumas linhas sejam omitidas. Nessa situação, pode ser definido o parâmetro de configuração row_security como off. Isso por si só não ignora a segurança de linha; o que faz é gerar um erro se os resultados de qualquer consulta forem filtrados por uma política. O motivo do erro pode então ser investigado e corrigido.

Nos exemplos acima, as expressões contidas nas políticas consideram apenas os valores correntes das linhas a serem acessadas ou modificadas. Esse é o caso mais simples e de melhor desempenho; quando for possível, é melhor projetar aplicações de segurança de linha para funcionarem dessa forma. Se for necessário consultar outras linhas, ou outras tabelas, para tomar uma decisão de política, isso pode ser feito usando sub‑SELECTs, ou funções que contenham SELECTs, nas expressões de políticas. Esteja ciente, no entanto, de que tais acessos podem criar condições de concorrência que podem permitir o vazamento de informações se não houver cuidado. Como exemplo, considere o seguinte projeto de tabela:

-- definição de grupos de privilégios
CREATE TABLE grupos (id_grupo int PRIMARY KEY,
                     nome_grupo text NOT NULL);

INSERT INTO grupos VALUES
  (1, 'baixo'),
  (2, 'médio'),
  (5, 'alto');

GRANT ALL ON grupos TO alice;  -- Alice é a administradora
GRANT SELECT ON grupos TO public;

-- definição dos níveis de privilégio dos usuários
CREATE TABLE usuarios (nome_usuario text PRIMARY KEY,
                       id_grupo int NOT NULL REFERENCES grupos);

INSERT INTO usuarios VALUES
  ('alice', 5),
  ('bob', 2),
  ('mallory', 2);

GRANT ALL ON usuarios TO alice;
GRANT SELECT ON usuarios TO public;

-- tabela contendo a informação a ser protegida
CREATE TABLE informacoes (informacao text,
                          id_grupo int NOT NULL REFERENCES grupos);

INSERT INTO informacoes VALUES
  ('quase secreto', 1),
  ('ligeiramente secreto', 2),
  ('muito secreto', 5);

ALTER TABLE informacoes ENABLE ROW LEVEL SECURITY;

-- a linha deve ser visível/atualizável pelos usuários cujo id_grupo
-- de segurança seja maior ou igual ao id_grupo da linha
CREATE POLICY fp_s ON informacoes FOR SELECT
  USING (id_grupo <= (SELECT id_grupo FROM usuarios WHERE nome_usuario = current_user));
CREATE POLICY fp_u ON informacoes FOR UPDATE
  USING (id_grupo <= (SELECT id_grupo FROM usuarios WHERE nome_usuario = current_user));

-- contamos apenas com segurança a nível de linha (RLS)
-- para proteger a tabela informacoes
GRANT ALL ON informacoes TO public;

Agora, suponha que alice queira mudar a informação ligeiramente secreto, mas acha que mallory não deve poder ver o novo conteúdo dessa linha, então ela faz:

BEGIN;
UPDATE usuarios SET id_grupo = 1 WHERE nome_usuario = 'mallory';
UPDATE informacoes SET informacao = 'escondida da mallory' WHERE id_grupo = 2;
COMMIT;

Isso parece seguro; não há nenhuma janela por onde mallory possa ver o texto escondida da mallory. No entanto, há uma condição de concorrência aqui. Se mallory estiver fazendo simultaneamente, digamos,

SELECT * FROM informacoes WHERE id_grupo = 2 FOR UPDATE;

e sua transação está no modo READ COMMITTED, é possível que mallory veja escondida da mallory. Isso vai acontecer se a transação da mallory atingir a linha da tabela informações logo após a transação da alice. A transação da mallory vai ficar bloqueada aguardando a transação da alice ser confirmada e, então, busca o conteúdo da linha atualizada graças à cláusula FOR UPDATE. No entanto, ela não busca a linha atualizada para o SELECT implícito da tabela usuarios, porque esse sub‑SELECT não tem FOR UPDATE; em vez disso, a linha da tabela usuarios é lida com o instantâneo obtido no início da consulta. Portanto, a expressão de política testa o valor antigo do nível de privilégio da mallory, permitindo que ela veja a linha atualizada.

Existem várias maneiras de contornar esse problema. Uma maneira simples é usar SELECT ... FOR SHARE em sub‑SELECTs nas políticas de segurança de linha. No entanto, isso requer a concessão do privilégio UPDATE na tabela referenciada (aqui usuarios) aos usuários afetados, o que pode ser indesejável. (Mas outra política de segurança de linha pode ser aplicada para impedir que eles realmente exerçam esse privilégio; ou o sub‑SELECT pode ser incorporado a uma função definidora de segurança.) Além disso, o uso simultâneo pesado de bloqueios de compartilhamento de linha na tabela referenciada pode representar um problema de desempenho, principalmente se as atualizações forem frequentes. Outra solução prática, se as atualizações da tabela referenciada forem pouco frequentes, é usar um bloqueio ACCESS EXCLUSIVE na tabela referenciada ao atualizá-la, para que nenhuma transação simultânea possa examinar valores de linha antigos. Ou pode-se apenas esperar que todas as transações simultâneas terminem após efetivar a atualização da tabela referenciada e antes de fazer alterações que dependam da nova situação de segurança.

Para obter detalhes adicionais veja CREATE POLICY e ALTER TABLE.

Dica

As políticas de segurança no nível de linha no banco de dados podem ser vistas consultando a visão do sistema Seção 53.15.



[35] covert channel: Um canal intra-sistema não intencional ou não autorizado permitindo que duas entidades cooperantes transfiram informações em uma forma que viole a política de segurança do sistema, mas não exceda as autorizações de acesso das entidades. (N. T.)