Lab 08: Security Definer Views โ Controlled Data Exposure¶
Neon Connection
Connect to your Neon Postgres instance for all work in this lab:
Branch naming convention:lab-08-<your-username> (e.g., lab-08-jsmith) Branch Requirement
Work on branch lab-08. All SQL runs on your Neon Postgres instance. Create the branch in the Neon console before starting, then update $DATABASE_URL to point to it.
Overview¶
A view is one of the most powerful and underused access-control tools in PostgreSQL. By granting users access to carefully designed views and revoking direct table access, you expose exactly the data each role needs โ no more, no less.
The security_barrier view option closes a subtle but real vulnerability: without it, a crafty attacker can craft a WHERE clause that causes PostgreSQL's query planner to evaluate their filter before the view's own filter, potentially leaking row data that should be hidden.
In this lab you implement a four-tier HR data exposure model โ public directory, manager view, payroll view, and HR admin view โ and confirm that direct table access is completely blocked for non-privileged roles.
By the end of this lab you will:
- Design a mixed-sensitivity table and immediately revoke direct access
- Create four views with progressively wider data exposure
- Apply
security_barrier = TRUEto prevent optimizer-based data leaks - Grant view access per role and verify that base-table access is denied
- Understand the limitations of view-based auditing vs trigger-based auditing
Part 1 โ Source Data with Mixed Sensitivity¶
Step 1.1 โ Create the HR schema and employee table¶
CREATE SCHEMA IF NOT EXISTS hr;
CREATE TABLE hr.employees (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
department TEXT NOT NULL,
-- CONFIDENTIAL fields
ssn TEXT NOT NULL,
salary NUMERIC(10,2) NOT NULL,
-- RESTRICTED fields
bank_account TEXT,
performance_rating INT CHECK (performance_rating BETWEEN 1 AND 5)
);
INSERT INTO hr.employees
(name, email, department, ssn, salary, bank_account, performance_rating)
VALUES
('Alice Chen', 'alice@corp.com', 'Engineering', '123-45-6789', 95000.00, 'ACCT-001', 5),
('Bob Smith', 'bob@corp.com', 'Marketing', '234-56-7890', 72000.00, 'ACCT-002', 3),
('Carol Davis', 'carol@corp.com', 'Finance', '345-67-8901', 115000.00, 'ACCT-003', 4),
('Dave Wilson', 'dave@corp.com', 'Engineering', '456-78-9012', 88000.00, 'ACCT-004', 4);
-- Immediately remove all public/default access to the raw table
REVOKE ALL ON hr.employees FROM PUBLIC;
Revoke First
Revoking PUBLIC access immediately after table creation is a best practice. New tables in PostgreSQL inherit a PUBLIC grant that allows any authenticated user to read them. Always revoke this before inserting sensitive data.
Part 2 โ Design Views by Access Level¶
Step 2.1 โ Tier 1: Public directory view (no PII)¶
CREATE OR REPLACE VIEW hr.v_directory
WITH (security_barrier = TRUE) AS
SELECT
id,
name,
email,
department
FROM hr.employees;
COMMENT ON VIEW hr.v_directory IS
'PUBLIC: name, email, department only. No salary, SSN, or bank data.';
Step 2.2 โ Tier 2: Manager view (salary + performance, no SSN/bank)¶
CREATE OR REPLACE VIEW hr.v_manager
WITH (security_barrier = TRUE) AS
SELECT
id,
name,
email,
department,
salary,
performance_rating
FROM hr.employees;
COMMENT ON VIEW hr.v_manager IS
'MANAGER: includes salary and performance rating. No SSN or bank account.';
Step 2.3 โ Tier 3: Payroll view (masked bank account, no SSN)¶
CREATE OR REPLACE VIEW hr.v_payroll
WITH (security_barrier = TRUE) AS
SELECT
id,
name,
department,
salary,
-- PCI-DSS pattern: mask everything except last 4 characters
'****' || RIGHT(bank_account, 4) AS bank_account_masked
FROM hr.employees;
COMMENT ON VIEW hr.v_payroll IS
'PAYROLL: salary and masked bank account. SSN is never exposed through this view.';
Step 2.4 โ Tier 4: HR admin view (full access with implicit access logging)¶
CREATE OR REPLACE VIEW hr.v_hr_admin AS
SELECT
*,
current_timestamp AS accessed_at,
current_user AS accessed_by
FROM hr.employees;
COMMENT ON VIEW hr.v_hr_admin IS
'HR ADMIN: Full access including SSN and bank account. Every SELECT records the accessor and timestamp.';
security_barrier on v_hr_admin
The HR admin view intentionally omits security_barrier because it is a full-exposure view with no filtering predicate to protect. The barrier is most important on views that have a WHERE clause filtering sensitive data.
Part 3 โ Grant Access via Views Only¶
Step 3.1 โ Create roles and grant view-level access¶
CREATE ROLE role_directory NOLOGIN;
CREATE ROLE role_manager NOLOGIN;
CREATE ROLE role_payroll NOLOGIN;
CREATE ROLE role_hr_admin NOLOGIN;
-- Schema access (required to resolve schema-qualified names)
GRANT USAGE ON SCHEMA hr
TO role_directory, role_manager, role_payroll, role_hr_admin;
-- Grant access to VIEWS only โ never to the base table
GRANT SELECT ON hr.v_directory TO role_directory;
GRANT SELECT ON hr.v_manager TO role_manager;
GRANT SELECT ON hr.v_payroll TO role_payroll;
GRANT SELECT ON hr.v_hr_admin TO role_hr_admin;
Step 3.2 โ Query each view and observe which columns are returned¶
-- Tier 1: directory (no salary, no SSN, no bank)
TABLE hr.v_directory;
-- Tier 2: manager (salary and performance, no SSN)
SELECT name, department, salary, performance_rating
FROM hr.v_manager;
-- Tier 3: payroll (masked bank account โ ****-NNNN format)
SELECT name, salary, bank_account_masked
FROM hr.v_payroll;
๐ธ Screenshot checkpoint โ capture all three result sets side-by-side or in sequence, showing each view returning only its designated columns.
Step 3.3 โ Verify that direct table access is blocked¶
SET ROLE role_directory;
-- Attempt to bypass the view and query the base table directly
SELECT * FROM hr.employees; -- must FAIL
RESET ROLE;
Expected:
๐ธ Screenshot checkpoint โ direct table access rejected for role_directory.
Step 3.4 โ Inventory all HR views and inspect their definitions¶
SELECT
viewname,
viewowner,
pg_get_viewdef(viewname::regclass, TRUE) AS definition
FROM pg_views
WHERE schemaname = 'hr'
ORDER BY viewname;
๐ธ Screenshot checkpoint โ all four views listed with their SQL definitions.
Assessment¶
Verification SQL¶
Dr. Chen will run the following query against your Neon connection string. Ensure all expected values are returned before submitting.
-- VERIFY LAB 08
SELECT
-- All four views exist in the hr schema
(SELECT COUNT(*)
FROM pg_views
WHERE schemaname = 'hr')::INT AS hr_view_count,
-- v_directory exposes NO sensitive columns (ssn, salary, bank_account)
(SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = 'hr'
AND table_name = 'v_directory'
AND column_name IN ('ssn', 'salary', 'bank_account'))::INT AS sensitive_cols_in_directory,
-- role_directory has SELECT on the directory view
has_table_privilege('role_directory', 'hr.v_directory', 'SELECT') AS dir_role_can_select,
-- role_directory does NOT have SELECT on the base table
has_table_privilege('role_directory', 'hr.employees', 'SELECT') AS dir_role_blocked_from_base;
Expected result: 4 | 0 | true | false
Additional Requirement (20 pts)¶
Create a hr.v_salary_bands view that replaces exact salary figures with human-readable salary bands, allowing managers to understand compensation distribution without exposing precise numbers.
Requirements:
-
Create the view using a
CASEexpression: -
Grant
role_managerSELECT on this view (in addition to their existingv_manageraccess). - Run a query as
role_manageronv_salary_bandsand show that it returns bands, not exact salaries. - Run a query as
role_manageronv_managerand show exact salaries are still returned via that view. - Write 2โ3 sentences explaining: in what HR or regulatory contexts is salary banding preferable to exact salary exposure, even for managers?
Submit: both queries with output and the written explanation.
Reflection Questions¶
-
security_barrier = TRUEprevents an optimizer-based data leak. Without this option, an attacker who has SELECT on the view can craft a WHERE clause on the view that causes the planner to evaluate their filter before the view's own WHERE clause, potentially leaking rows that should be hidden. Give a concrete SQL example of this attack against a hypothetical viewhr.v_active_employees(which hasWHERE is_active = TRUE) that lackssecurity_barrier. Explain step-by-step how the planner reordering reveals inactive employees. -
You granted access to views and revoked it from the base table. What happens if a developer later runs
ALTER TABLE hr.employees ADD COLUMN medical_notes TEXTand inserts sensitive data? Is that new column automatically protected by the existing views? What organizational process should be established to ensure new columns are reviewed for sensitivity before data is inserted? -
The
v_hr_adminview appendsaccessed_atandaccessed_bycolumns. This is a lightweight, zero-infrastructure auditing approach. What are three specific limitations of this view-based audit compared to a proper trigger-based audit log (as implemented in Lab 09)? Describe a scenario where view-based logging would be clearly insufficient for a compliance audit.
Grading Rubric
| Component | Points |
|---|---|
| Lab steps (Parts 1โ3 with screenshots) | 50 |
| Verification SQL matches expected output | 30 |
| Additional Requirement (v_salary_bands) | 20 |
| Total | 100 |