Oracle VPD as a safeguard for DML

Virtual Private Databases (VPD) is an Enterprise Edition feature related to security. It restricts the scope of Data Manipulation Language to a subset of the table rows by transparently adding a where clause before executing them. It is also called Row-Level Security (RLS). Where the policy is enabled, it is like having the selected DML (SELECT, INSERT, UPDATE, DELETE) operate on a transcient view. And the predicates for this view can be dynamic and even query tables that the user cannot see.

In this post I'll show how to use it not as a strict restriction, but as a safeguard for users with powerful roles. A user has all DML granted. He usually queries and modify the tables through the application. But there may be some exceptional changes to do that were not forseen and were not implemented in the application. For this, the user has access to the production tables through SQL Developer. This is for users who knows what they do, with full knowledge of the data model. But human error can always happen and a mistake in a where clause can be critical. In order to reduce the risk, we can limit the scope of rows that are modified with Row Level Security. With its default role, the user can modify only the rows that are related to his area.

I run the example on SCOTT schema. The user will have any DML granted, but the VPD will prevent any modification of employees (EMP table) that are not in the 'SALES' department (DEPT). The restriction is based on the role which gives the DML privileges.

Define the VPD 

I create a role DEMO_SALES and a user DEMO_SALES_USER with this role granted.

With this role, the user can read EMP and DEPT from the SCOTT schema, and do any DML on EMP

SQL> connect sys/oracle@//localhost/PDB1 as sysdba
Connected.

SQL> create role DEMO_SALES;

Role created.

SQL> grant select, insert, delete, update on scott.EMP to DEMO_SALES;

Grant succeeded.

SQL> grant select on scott.DEPT to DEMO_SALES;

Grant succeeded.

SQL> grant connect,demo_sales to DEMO_SALES_USER identified by demo_sales_user;

Grant succeeded.

I want to prevent that users with this role can accidently modify EMP rows belonging to other department than SALES

In order to create the VPD policy, SCOTT needs to execute DBMS_RLS (warning: this allows to disable or drop on any schema in the database)

SQL> connect sys/oracle@//localhost/PDB1 as sysdba
Connected.
SQL Format Cleared
SQL> grant execute on dbms_rls to scott;

Grant succeeded.

SCOTT creates the VPD policy to view only SALES employees for INSERT, DELETE and UPDATE statements

SQL> connect scott/tiger@//localhost/PDB1
Connected.
SQL Format Cleared
SQL> select * from SCOTT.DEPT;

    DEPTNO DNAME          LOC          
---------- -------------- -------------
        10 ACCOUNTING     NEW YORK     
        20 RESEARCH       DALLAS       
        30 SALES          CHICAGO      
        40 OPERATIONS     BOSTON       

SQL> create function SCOTT.DEMO_VPD_FUNCTION(object_schema IN VARCHAR2, object_name VARCHAR2) return varchar2 as
  2  begin
  3   return q'[DEPTNO in (select DEPTNO from DEPT where DNAME='SALES') or 'DEMO_SALES' not in (select role from session_roles)]';
  4  end;
  5  /

Function created.
SQL> show errors
SQL> exec dbms_rls.add_policy(object_schema=>null,object_name=>'EMP'-
>  ,policy_name=>'DEMO_POLICY', function_schema=>null,policy_function=>'DEMO_VPD_FUNCTION'-
>  ,statement_types=>'INSERT,UPDATE,DELETE',update_check=>true,enable=>true);

PL/SQL procedure successfully completed.

Test the VPD 

I connect as DEMO_SALES_USER which has the DEMO_SALES role by default

SQL> connect DEMO_SALES_USER/demo_sales_user@//localhost/PDB1
Connected.
SQL Format Cleared
SQL> select role from session_roles;

ROLE
--------------------------------------------------------------------------------------------------------------------------------
CONNECT
DEMO_SALES

I can see all rows from EMP

SQL> select * from SCOTT.DEPT join SCOTT.EMP using(DEPTNO) order by DNAME;

    DEPTNO DNAME          LOC                EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM
---------- -------------- ------------- ---------- ---------- --------- ---------- --------- ---------- ----------
        10 ACCOUNTING     NEW YORK            7782 CLARK      MANAGER         7839 09-JUN-81       2450           
        10 ACCOUNTING     NEW YORK            7839 KING       PRESIDENT            17-NOV-81       5000           
        10 ACCOUNTING     NEW YORK            7934 MILLER     CLERK           7782 23-JAN-82       1300           
        20 RESEARCH       DALLAS              7566 JONES      MANAGER         7839 02-APR-81       2975           
        20 RESEARCH       DALLAS              7902 FORD       ANALYST         7566 03-DEC-81       3000           
        20 RESEARCH       DALLAS              7876 ADAMS      CLERK           7788 23-MAY-87       1100           
        20 RESEARCH       DALLAS              7369 SMITH      CLERK           7902 17-DEC-80        800           
        20 RESEARCH       DALLAS              7788 SCOTT      ANALYST         7566 19-APR-87       3000           
        30 SALES          CHICAGO             7521 WARD       SALESMAN        7698 22-FEB-81       1250        500
        30 SALES          CHICAGO             7844 TURNER     SALESMAN        7698 08-SEP-81       1500          0
        30 SALES          CHICAGO             7499 ALLEN      SALESMAN        7698 20-FEB-81       1600        300
        30 SALES          CHICAGO             7900 JAMES      CLERK           7698 03-DEC-81        950           
        30 SALES          CHICAGO             7698 BLAKE      MANAGER         7839 01-MAY-81       2850           
        30 SALES          CHICAGO             7654 MARTIN     SALESMAN        7698 28-SEP-81       1250       1400

14 rows selected. 

Trying to update all salaries, only those from department SALES are actually modified

SQL> update SCOTT.EMP set sal=sal+10000;

6 rows updated.

No errors. This is like a view: the DML operates on the subset of rows defined by the VPD.

Inserting for departement SALES is allowed

SQL> insert into SCOTT.EMP(EMPNO,ENAME,DEPTNO,SAL) values (8001,'Sales',30,10000);

1 row created.

Inserting for other departements raises an error

SQL> insert into SCOTT.EMP(EMPNO,ENAME,DEPTNO,SAL) values (8002,'No Sales',20,10000);

insert into SCOTT.EMP(EMPNO,ENAME,DEPTNO,SAL) values (8002,'No Sales',20,10000)
                  *
ERROR at line 1:
ORA-28115: policy with check option violation

This goes beyond viewing only a subset of rows. Because the VPD was created with update_check=>true the modifications that create rows outside of the view raises an error.

A delete removes only the rows from SALES departement

SQL> delete from SCOTT.EMP;

7 rows deleted.

Basically all other department were protected from any DML mistake

SQL> select * from SCOTT.DEPT join SCOTT.EMP using(DEPTNO) order by DNAME;

    DEPTNO DNAME          LOC                EMPNO ENAME      JOB              MGR HIREDATE         SAL       COMM
---------- -------------- ------------- ---------- ---------- --------- ---------- --------- ---------- ----------
        10 ACCOUNTING     NEW YORK            7934 MILLER     CLERK           7782 23-JAN-82       1300           
        10 ACCOUNTING     NEW YORK            7782 CLARK      MANAGER         7839 09-JUN-81       2450           
        10 ACCOUNTING     NEW YORK            7839 KING       PRESIDENT            17-NOV-81       5000           
        20 RESEARCH       DALLAS              7902 FORD       ANALYST         7566 03-DEC-81       3000           
        20 RESEARCH       DALLAS              7876 ADAMS      CLERK           7788 23-MAY-87       1100           
        20 RESEARCH       DALLAS              7566 JONES      MANAGER         7839 02-APR-81       2975           
        20 RESEARCH       DALLAS              7369 SMITH      CLERK           7902 17-DEC-80        800           
        20 RESEARCH       DALLAS              7788 SCOTT      ANALYST         7566 19-APR-87       3000           

8 rows selected. 

If we grant DML through another role or direct grant, the protection is still there

SQL> connect scott/tiger@//localhost/PDB1
Connected.
SQL Format Cleared
SQL> grant all on SCOTT.EMP to DEMO_SALES_USER;

Grant succeeded.

SQL> grant all on SCOTT.DEPT to DEMO_SALES_USER;

Grant succeeded.

SQL> connect DEMO_SALES_USER/demo_sales_user@//localhost/PDB1
Connected.
SQL Format Cleared
SQL> delete from SCOTT.EMP;

0 rows deleted.

As I have defined it, bypassing this restriction requires to disable the role from the session

SQL> set role none;

Role succeeded.

SQL> select role from session_roles;
no rows selected
SQL> delete from SCOTT.EMP;

8 rows deleted.

Here all rows were deleted because the safeguard role was disabled, and another privilege was used to run the DML.

SQL> select * from SCOTT.EMP ;
no rows selected
SQL> exit

In summary, this solution is not there to block the modifications on non-allowed department. The user has all privileges on purpose, because he may need them. The idea is just to add a safeguard to be sure that the part of data which is not in the normal scope of modifications is not altered by mistake. 

Comments

Thank you for the post

Add new comment