PostgreSQL Bài 40: Bảo mật và Phân quyền với Role, Grant và Row-Level Security (RLS)
Bước vào giai đoạn Production, dữ liệu không chỉ cần chạy nhanh và an toàn trước sự cố đứt điện, mà còn phải được bảo vệ tuyệt đối trước con người. Một nguyên tắc sống còn trong quản trị cơ sở dữ liệu: Không bao giờ cho phép ứng dụng Backend kết nối bằng tài khoản postgres (Superuser).
Hệ thống bảo mật của PostgreSQL được phân lớp cực kỳ chặt chẽ, từ cấp độ toàn cục (Instance) xuống tới cấp độ Cơ sở dữ liệu (Database), Schema, Bảng (Table), và đỉnh cao nhất là cấp độ từng Dòng dữ liệu (Row-Level Security). Bài học này sẽ giúp bạn thiết lập một ma trận phân quyền chuẩn mực.
1. Bản chất của Role và User trong PostgreSQL
Nhiều hệ quản trị tách biệt khái niệm "Người dùng" (User) và "Nhóm quyền" (Group/Role). Trong PostgreSQL, chúng là một. Khái niệm chuẩn xác nhất là Role.
Một Role có thể đóng vai trò là một User (nếu nó có quyền đăng nhập) hoặc đóng vai trò là một Group (để chứa các Role khác).
SQL
-- Tạo một Role dùng làm Nhóm quyền (Không được phép đăng nhập)
CREATE ROLE read_only_group;
-- Tạo một Role dùng làm User (Có quyền đăng nhập và mật khẩu)
CREATE ROLE backend_api_user WITH LOGIN PASSWORD 'secret_pass';
-- Lệnh CREATE USER thực chất chỉ là bí danh của CREATE ROLE WITH LOGIN
Kế thừa quyền hạn (Role Inheritance)
Sức mạnh của Role nằm ở khả năng kế thừa. Thay vì cấp quyền lắt nhắt cho từng lập trình viên, bạn cấp quyền cho Group, sau đó đưa User vào Group đó.
SQL
-- Gán user 'backend_api_user' vào nhóm 'read_only_group'
GRANT read_only_group TO backend_api_user;
2. Ma trận Phân quyền (GRANT / REVOKE)
Để một Role có thể thực thi lệnh SELECT trên bảng users, nó phải vượt qua 3 cánh cửa bảo vệ liên tiếp. Thiếu một trong ba, lệnh truy vấn sẽ bị từ chối ngay lập tức.
Cửa 1: Quyền truy cập Cơ sở dữ liệu (Database)
Role phải được phép kết nối vào database.
SQL
GRANT CONNECT ON DATABASE my_company_db TO read_only_group;
Cửa 2: Quyền truy cập Lược đồ (Schema)
Database chứa nhiều Schema (như public, auth, tenant_a). Bạn phải cho phép Role nhìn thấy Schema.
SQL
GRANT USAGE ON SCHEMA public TO read_only_group;
Cửa 3: Quyền thao tác trên Bảng (Table/View)
Sau khi đã vào được Schema, Role mới được cấp quyền thao tác DML (SELECT, INSERT, UPDATE, DELETE).
SQL
-- Cấp quyền SELECT trên một bảng cụ thể
GRANT SELECT ON TABLE public.users TO read_only_group;
-- Cấp quyền SELECT trên TẤT CẢ các bảng hiện có trong schema
GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only_group;
-- Cấp quyền thực thi các hàm (Functions/Procedures)
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO read_only_group;
Cạm bẫy Default Privileges:
Lệnh
GRANT ... ON ALL TABLESchỉ áp dụng cho các bảng đã tồn tại tại thời điểm chạy lệnh. Nếu ngày mai bạn tạo thêm bảng mới,read_only_groupsẽ không có quyền truy cập bảng đó!Để giải quyết, hãy sử dụng
ALTER DEFAULT PRIVILEGES:SQL
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO read_only_group;
3. Row-Level Security (RLS): Cuộc cách mạng cho ứng dụng Multi-Tenant
Các quyền GRANT ở trên hoạt động ở cấp độ bảng (Table-level). Nhưng nếu bạn xây dựng một nền tảng SaaS có hàng ngàn công ty (Tenants) cùng sử dụng chung một bảng orders, làm sao để Công ty A không bao giờ nhìn thấy đơn hàng của Công ty B?
Giải pháp cũ: Backend phải nhớ thêm điều kiện WHERE tenant_id = 'A' vào mọi câu lệnh SQL. Chỉ cần một lập trình viên quên viết WHERE, dữ liệu sẽ bị rò rỉ chéo.
Giải pháp hiện đại: Row-Level Security (RLS).
Khi RLS được bật, PostgreSQL đóng vai trò như một bộ lọc tàng hình ở tầng database. Nó tự động gá thêm điều kiện lọc vào mọi câu lệnh truy vấn trước khi thực thi, bảo vệ dữ liệu ở mức vật lý bất chấp việc Backend có gọi thiếu mệnh đề WHERE hay không.
Bước 1: Kích hoạt RLS trên bảng
SQL
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
amount NUMERIC,
status TEXT
);
-- Bật tính năng RLS (Nếu không có Policy nào, bảng sẽ mặc định tàng hình với mọi user)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
Bước 2: Viết Chính sách (Policy)
Chúng ta sẽ tận dụng một biến môi trường (Session variable) để PostgreSQL biết user hiện tại đang thuộc Tenant nào.
SQL
-- Tạo chính sách: Chỉ cho phép đọc/ghi các dòng có tenant_id trùng với biến môi trường 'app.current_tenant'
CREATE POLICY tenant_isolation_policy ON orders
AS PERMISSIVE
FOR ALL
TO backend_api_user
USING (tenant_id = current_setting('app.current_tenant', TRUE)::INT);
Bước 3: Thử nghiệm sức mạnh của RLS
Từ ứng dụng Backend, khi kết nối mở ra, bạn bơm ID của Tenant vào Session:
SQL
-- 1. Backend xác thực user thuộc Công ty số 5
SET LOCAL app.current_tenant = '5';
-- 2. Lập trình viên chạy lệnh lấy toàn bộ đơn hàng (QUÊN viết mệnh đề WHERE)
SELECT * FROM orders;
Kết quả: PostgreSQL tự động ngầm biến đổi câu lệnh thành SELECT * FROM orders WHERE tenant_id = 5;. Lập trình viên không thể lấy được dữ liệu của công ty khác, dù cố tình hay vô ý. Lệnh UPDATE hoặc DELETE cũng bị giới hạn tương tự.
4. Các điểm mù và Kỹ thuật tối ưu RLS
4.1. Ai có quyền xuyên thủng RLS?
RLS cực kỳ nghiêm ngặt, nhưng nó sẽ bị vô hiệu hóa hoàn toàn đối với 2 đối tượng:
-
Superuser (như tài khoản
postgres). -
Owner (Tài khoản tạo ra bảng đó).
Nếu bạn muốn chính người tạo ra bảng cũng bị áp dụng RLS (thường dùng để test), bạn phải chạy thêm lệnh ép buộc:
SQL
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
4.2. Cạm bẫy hiệu năng của hàm trong Policy
Mệnh đề USING (...) của RLS được dán trực tiếp vào mệnh đề WHERE của mỗi câu truy vấn.
Nếu bạn viết một hàm phức tạp (ví dụ: query sang bảng khác để check quyền) bên trong Policy, hàm đó có thể bị gọi trên từng dòng dữ liệu (Per-row evaluation), khiến một câu lệnh 10ms biến thành 10 giây.
Quy tắc vàng khi dùng RLS:
-
Cột được dùng trong
USING(nhưtenant_id) bắt buộc phải có B-Tree Index. -
Ưu tiên đọc biến Session (
current_setting) hoặccurrent_uservì chúng là thao tác trên RAM, thời gian thực thi là . -
Nếu phải dùng hàm (Function) trong Policy, đảm bảo hàm đó nhẹ và thử cấu hình nó là
STABLEđể PostgreSQL có thể cache kết quả trong 1 transaction.
Nền tảng Supabase được xây dựng dựa trên chính cơ chế RLS này để cho phép Frontend/Mobile gọi thẳng vào Database một cách an toàn mà không cần viết Backend API trung gian.
All rights reserved