Oracle VPD in Action: Building Role-Based Row-Level Security from Scratch

Row-level security is one of those requirements that shows up in almost every enterprise database — Finance shouldn’t see HR’s salary data, HR shouldn’t see Finance’s, and no team should see another’s unless explicitly authorized.

Most teams solve this at the application layer, hard-coding WHERE clauses into every query. It works — until someone connects directly through SQL Developer or a reporting tool and bypasses the app entirely.

Oracle’s Virtual Private Database (VPD), also known as Fine-Grained Access Control (FGAC), solves this properly: it enforces row-level and column-level security inside the database engine itself, so it’s enforced everywhere, every time — no exceptions.

In this post, I’ll walk through how I implemented it, mapping database roles to departments on a sample emp_test table.

How VPD Works

  1. Policy Association – A security policy is bound to a table or view using the DBMS_RLS (Row Level Security) package.
  2. Dynamic Interception – Any SELECT, INSERT, UPDATE, or DELETE against the protected table is intercepted by Oracle.
  3. Policy Evaluation – A PL/SQL policy function runs to determine the security rule for the current session.
  4. Predicate Appending – The function returns a string (e.g. department = 'FINANCE'), which Oracle silently appends to the query.
  5. Execution – The user only ever sees rows they are authorized for — completely transparent to the client application.

Key Benefits

  • Transparent security — no custom query logic needed in the application
  • Granular control — restrict by role, session attribute, application context, or even time of day
  • Protection against direct access — enforced even via SQL*Plus, SQL Developer, or third-party reporting tools
  • Reduced maintenance — no more duplicate views/tables per user segment

Step 1: Create the Test Table and Sample Data

CREATE TABLE emp_test (
  emp_id NUMBER,
  emp_name VARCHAR2(50),
  department VARCHAR2(50),
  salary NUMBER
);

INSERT INTO emp_test VALUES (1, 'Munish', 'IT', 85000);
INSERT INTO emp_test VALUES (2, 'Deepak', 'MAKT', 90000);
INSERT INTO emp_test VALUES (3, 'Sanjay', 'FINANCE', 75000);
INSERT INTO emp_test VALUES (4, 'Bikash', 'HR', 65000);
COMMIT;

Step 2: Create the Application Context

An application context securely stores session-specific attributes — such as the user’s mapped department — so the policy function can read them efficiently.

CREATE OR REPLACE PACKAGE emp_ctx_pkg AS
  PROCEDURE set_department;
END emp_ctx_pkg;
/

CREATE OR REPLACE PACKAGE BODY emp_ctx_pkg AS
  PROCEDURE set_department IS
    v_role VARCHAR2(50);
    v_dept VARCHAR2(50);
  BEGIN
    IF v_role = 'IT' THEN
      v_dept := 'IT';
    ELSIF v_role = 'MAKT' THEN
      v_dept := 'MAKT';
    ELSIF v_role = 'FINANCE' THEN
      v_dept := 'FINANCE';
    ELSIF v_role = 'HR' THEN
      v_dept := 'HR';
    ELSE
      v_dept := 'ALL'; -- Admin / Manager
    END IF;
    DBMS_SESSION.SET_CONTEXT('emp_security_ctx', 'user_dept', v_dept);
  END set_department;
END emp_ctx_pkg;
/

CREATE CONTEXT emp_security_ctx USING emp_ctx_pkg;

Step 3: Create the Policy Function

The policy function returns the predicate (WHERE clause) that Oracle appends to any query against the protected table.

CREATE OR REPLACE PACKAGE emp_sec_fn_pkg AS
  FUNCTION f_security_predicate (
    schema_p IN VARCHAR2,
    table_p  IN VARCHAR2
  ) RETURN VARCHAR2;
END emp_sec_fn_pkg;
/

CREATE OR REPLACE PACKAGE BODY emp_sec_fn_pkg AS
  FUNCTION f_security_predicate (
    schema_p IN VARCHAR2,
    table_p  IN VARCHAR2
  ) RETURN VARCHAR2 IS
    v_user_dept VARCHAR2(50);
    v_predicate VARCHAR2(4000);
  BEGIN
    v_user_dept := SYS_CONTEXT('emp_security_ctx', 'user_dept');

    IF v_user_dept = 'ALL' THEN
      RETURN ''; -- No predicate added (returns all rows)
    END IF;

    v_predicate := 'department = ''' || v_user_dept || '''';
    RETURN v_predicate;
  END f_security_predicate;
END emp_sec_fn_pkg;
/

Step 4: Add the VPD Policy Using DBMS_RLS

BEGIN
  DBMS_RLS.ADD_POLICY (
    object_schema   => 'SYSTEM',
    object_name     => 'emp_test',
    policy_name     => 'emp_dept_policy',
    function_schema => 'SYSTEM',
    policy_function => 'emp_sec_fn_pkg.f_security_predicate',
    statement_types => 'SELECT, UPDATE, DELETE'
  );
END;
/

BEGIN
  DBMS_RLS.ENABLE_POLICY(
    object_schema => 'SYSTEM',
    object_name   => 'emp_test',
    policy_name   => 'emp_dept_policy',
    enable        => TRUE
  );
END;
/

Key parameters:

  • statement_types — which DML operations trigger the policy
  • update_check — when TRUE, Oracle validates that inserted/updated rows still satisfy the policy predicate, preventing a user from writing a row that violates their own security rule

Step 5: Test the Implementation

-- Create Database Roles
CREATE ROLE it;
CREATE ROLE makt;
CREATE ROLE finance;
CREATE ROLE hr;

-- Grant Privileges
GRANT connect, resource TO it;
GRANT connect, resource TO makt;
GRANT connect, resource TO hr;
GRANT connect, resource TO finance;

GRANT SELECT, INSERT, UPDATE ON system.emp_test TO finance;
GRANT SELECT, INSERT, UPDATE ON system.emp_test TO makt;
GRANT SELECT, INSERT, UPDATE ON system.emp_test TO hr;
GRANT SELECT, INSERT, UPDATE ON system.emp_test TO it;

-- Create test database users
CREATE USER finance_user IDENTIFIED BY password123;
CREATE USER it_user IDENTIFIED BY password123;
CREATE USER makt_user IDENTIFIED BY password123;
CREATE USER hr_user IDENTIFIED BY password123;

-- Grant roles to users
GRANT finance TO finance_user;
GRANT it TO it_user;
GRANT makt TO makt_user;
GRANT hr TO hr_user;

-- Allow every role to execute the context package
CREATE OR REPLACE PUBLIC SYNONYM emp_ctx_pkg FOR SYSTEM.emp_ctx_pkg;
GRANT EXECUTE ON emp_ctx_pkg TO it;
GRANT EXECUTE ON emp_ctx_pkg TO makt;
GRANT EXECUTE ON emp_ctx_pkg TO finance;
GRANT EXECUTE ON emp_ctx_pkg TO hr;

Once each user logs in and their session context is initialized, querying emp_test transparently returns only the rows belonging to their department — with zero application-level filtering.

Closing Thoughts

VPD is one of the most underused security features in Oracle — it moves access control out of fragile application code and into the database itself, where it can’t be bypassed. For any organization handling multi-department or multi-tenant data, it’s worth serious consideration.

Screenshots of the role grants, user creation, and query verification are included in the original documentation — feel free to reach out if you’d like the full write-up or a walkthrough.

Labels/Tags: Oracle, VPD, Fine-Grained Access Control, Row Level Security, PL/SQL, DBMS_RLS, Database Security, Oracle DBA

Comments

Popular posts from this blog

Strengthening Database Security with SQL Firewall in Oracle 26ai

MySQL Installation on Oracle Cloud’s “Always Free” Compute Instance

MySQL Replication on Oracle Cloud’s “Always Free” Compute Instance