Oracle Virtual Private Database

Oracle Virtual Private Database (VPD) is an awesome tool you can use to secure your data. It allows you to restrict data on row and/or column level for each user or role individually. It works for all tables, views, and synonyms where VPD was applied by appending the WHERE clause. It is actually awesome because there is no way for anybody to bypass this security layer 🙂

Whenever a user issues a query (SELECT, UPDATE, INSERT, DELETE, and CREATE/ALTER INDEX), directly or indirectly, Oracle Virtual Private Database automatically updates the query with a predefined WHERE clause.

Let’s demonstrate it in the following example:

-- user A issues SELECT query on an employees table
-- this user is allowed to see only his own direct reports and no one else

SELECT * 
FROM oracle_world.employee;

-- Oracle Virtual Private Database is applied to this table 
-- therefore the original query is automatically transformed to this

SELECT *
FROM oracle_world.employee
WHERE superior_employee = 10001 -- requestors ID

-- or you can use sys_context (application context) variables as well

SELECT *
FROM oracle_world.employee
WHERE superior_employee = SYS_CONTEXT('USERENV','OS_USER');

Benefits

Oracle Virtual Private Database is really a cool feature and its main advantage is a very simple, flexible, and secure implementation. Usually, all DBAs handle the security on the application level, which might, of course, lead to several issues and holes. With Oracle VPD you can be sure that all security rules will not be bypassed, either by a coincidence or on purpose, since the rules are implemented directly to the object.

Another great thing is you can configure all statements individually. Having said that, you can define one rule for SELECT, another for INSERT, different for UPDATE, and something totally else for DELETE. So you can easily let someone SELECT everything in the table but they can be limited to UPDATE only certain records without the possibility to INSERT. That’s awesome 🙂

Drawbacks

Each coin has two sides and Oracle VPD is no exception. You need to be careful not to “overdo” the logic and keep it simple or you can run into performance issues. On top of that, since the system is adding WHERE clause, the corresponding objects must be optimized accordingly (proper indexes, partitions, ….)

Policy Evaluation

There are three ways the VPD’s policy can be evaluated:

  • Static – once per query
  • Context-aware – only when the application context within the policy changes
  • Dynamic – every runtime

I highly suggest sticking with context-aware policies. They allow you to have better and dynamic control over the access to your protected objects.

Furthermore, you can also create functions and assigned them to a policy. There are certain requirements for such functions such as:

  • Must return VARCHAR2 data type
  • Must generate a valid WHERE clause
  • Must be a pure function
  • Must take schema name and object name as input arguments

List of procedures (policies)

Ok, let’s cut the theory and start with something for enjoyable 🙂 To be able to use VPD, you have to have DMBS_RLS package in the database and, of course, you have to be allowed to use it 🙂 Here is the list of procedures you can use.

Individual handling

ProcedureDescription
DBMS_RLS.ADD_POLICYAdds the policy to an object
DBMS_RLS.DROP_POLICYRemoves the policy from an object
DBMS_RLS.ENABLE_POLICYEnables/Disables added policy
DBMS_RLS.ALTER_POLICYAlters an existing policy to add or remove attributes
DBMS_RLS.REFRESH_POLICYInvalidates cursors (non-static policies only)

Group handling

ProcedureDescription
DBMS_RLS.CREATE_POLICY_GROUPCreates a policy group
DBMS_RLS.DELETE_POLICY_GROUPDeletes the whole policy group
DBMS_RLS.ADD_GROUPED_POLICYAdds a policy (to a specified group)
DBMS_RLS.DROP_GROUPED_POLICYDeletes a policy (from a specified group)
DBMS_RLS.ENABLE_GROUPED_POLICYEnables a policy (within a group)
DBMS_RLS.DISABLE_GROUPED_POLICYDisable a policy (within a group)
DBMS_RLS.ALTER_GROUPED_POLICYAlters a policy group
DBMS_RLS.REFRESH_GROUPED_POLICYRe-parses again all SQL statements associate with the affected policy

Application Context handling

ProcedureDescription
DBMS_RLS.CREATE_POLICY_GROUPCreates a policy group
DBMS_RLS.DELETE_POLICY_GROUPDeletes the whole policy group
DBMS_RLS.ADD_GROUPED_POLICYAdds a policy (to a specified group)
DBMS_RLS.DROP_GROUPED_POLICYDeletes a policy (from a specified group)
DBMS_RLS.ENABLE_GROUPED_POLICYEnables a policy (within a group)
DBMS_RLS.DISABLE_GROUPED_POLICYDisable a policy (within a group)
DBMS_RLS.ALTER_GROUPED_POLICYAlters a policy group
DBMS_RLS.REFRESH_GROUPED_POLICYRe-parses again all SQL statements associate with the affected policy

Examples

Let’s check some examples of usage.

Example1: Restrict access to contact columns (email, phone) for everybody except for the application account (=WEBAPP)

-- FUNCTION to limit the records
CREATE OR REPLACE FUNCTION f_restrict_contact (i_schema IN VARCHAR2, i_objname IN VARCHAR2)
RETURN VARCHAR2 AS 
  where_limit VARCHAR2 (200);
BEGIN

 IF user = 'WEBAPP' THEN
   where_limit := '1=1'; 
 ELSE
   where_limit := '1=2';
 END IF;
 RETURN (where_limit);
END f_restrict_contact;
/

-- add policy
BEGIN
 DBMS_RLS.ADD_POLICY (
  object_schema         => 'oracle_world', 
  object_name           => 'customer',
  policy_name           => 'restrict_contact', 
  policy_function       => 'f_restrict_contact',
--policy_function       => 'pckg_restriction.f_restrict_contact', -- in case your function is part of a package
  sec_relevant_cols     => 'email,phone',
  sec_relevant_cols_opt => dbms_rls.all_rows
  );
END;
/

-- when the following query is issued
SELECT name, phone, email
FROM oracle_world.customer;

-- this will be the output for WEBAPP
NAME         PHONE          EMAIL
-----------  -------------  --------------------------
John Doe     234 878 9093   john.doe@oracle-world.com  
Jane Black   125 819 1103   jane.black@oracle-world.com
Jimmy Red    103 220 0203   jimmy.red@oracle-world.com
Maria Creed  430 003 1100   maria.creed@oracle-world.com
Donny Dark   329 810 0182   donny.dark@oracle-world.com

-- this will be the output for any other user
NAME        PHONE         EMAIL 
----------- ------------- -------------------------- 
John Doe      
Jane Black 
Jimmy Red 
Maria Creed  
Donny Dark 

--drop the policy
BEGIN
 DBMS_RLS.DROP_POLICY (
   object_schema  => 'oracle_world',
   object_name    => 'customer', 
   policy_name    => 'restrict_contact'
 );
END;

If you don’t want to show the restricted rows at all, remove the “sec_relevant_cols_opt => dbms_rls.all_rows” from the ADD_POLICY. This only makes sure it will show all columns with those restricted being NULL.

As you can see, those lines with different id_department will not be displayed; cool, right? 🙂

There, of course, other ways you can use the functions.

Example2: Limit access to data for a specific role.

-- FUNCTION to limit the records
CREATE OR REPLACE FUNCTION f_restrict_contact (i_schema IN VARCHAR2, i_objname IN VARCHAR2)
RETURN VARCHAR2 AS 
  where_limit VARCHAR2 (200) DEFAULT NULL;
  l_has_role CHAR (1);
BEGIN
  
 SELECT DECODE(count(*), 0, 'N', 'Y') INTO l_has_role
 FROM dba_role_privs
 WHERE 1=1
 AND grantee = user
 AND granted_role = 'ROLE_MANAGER';
 IF l_has_role = 'N' THEN
   where_limit := '1=2'; 
 ELSE
   NULL
 END IF;
 RETURN (where_limit);
END f_restrict_contact;
/

You can get very creative with the roles, restrictions, group policies, and the whatnot. Oracle Virtual Private database can be very flexible and allows you to set extremely granular restrictions; just keep in mind the performance 🙂

If you are keen to learn more, dive into the official documentation where every single detail is described.