Nỗi ám ảnh mang tên “nhầm dữ liệu” khách hàng
Nếu bạn đang xây dựng ứng dụng SaaS (Software as a Service), chắc chắn bạn đã từng lo lắng về việc dữ liệu công ty A bị rò rỉ sang công ty B. Cách làm truyền thống là thêm điều kiện WHERE tenant_id = 'công-ty-A' vào mọi câu lệnh SELECT, UPDATE, DELETE.
Tuy nhiên, thực tế triển khai thường không đơn giản. Chỉ cần một lập trình viên quên thêm WHERE trong một API mới, toàn bộ dữ liệu khách hàng có thể bị phơi bày. Row Level Security (RLS) của PostgreSQL đóng vai trò như một “chốt chặn” cuối cùng ngay tại tầng database. Nó giúp bảo mật trở thành một phần nội tại của hệ thống, giúp code ứng dụng gọn gàng và an toàn hơn nhiều.
Quick start: Triển khai RLS trong 5 phút
Hãy thử thiết lập một tình huống thực tế: Bạn có bảng orders (đơn hàng) và muốn đảm bảo mỗi nhân viên chỉ thấy được đơn hàng do chính mình quản lý.
Bước 1: Khởi tạo bảng và dữ liệu mẫu
-- Tạo các user riêng biệt
CREATE USER dev_tung WITH PASSWORD 'pass123';
CREATE USER dev_hoa WITH PASSWORD 'pass123';
-- Tạo bảng đơn hàng
CREATE TABLE orders (
id serial PRIMARY KEY,
item_name text,
owner name DEFAULT current_user
);
-- Cấp quyền cơ bản
GRANT SELECT, INSERT, UPDATE, DELETE ON orders TO dev_tung, dev_hoa;
-- Chèn dữ liệu thử nghiệm
INSERT INTO orders (item_name, owner) VALUES ('Macbook M3', 'dev_tung');
INSERT INTO orders (item_name, owner) VALUES ('Dell XPS', 'dev_hoa');
Bước 2: Kích hoạt RLS và định nghĩa Policy
Mặc định, Postgres cho phép mọi user có quyền truy cập bảng thấy toàn bộ dữ liệu. Chúng ta cần bật cơ chế bảo mật cấp dòng và đặt ra luật lệ.
-- Bật tính năng RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Tạo chính sách: Chỉ truy cập dòng có owner trùng với user đang kết nối
CREATE POLICY order_ownership_policy ON orders
USING (owner = current_user);
Bước 3: Kiểm chứng kết quả
Khi bạn đăng nhập bằng dev_tung và chạy SELECT * FROM orders;, kết quả sẽ chỉ hiển thị dòng ‘Macbook M3’. Postgres đã tự động “tiêm” thêm bộ lọc vào truy vấn của bạn mà không cần bạn sửa một dòng code app nào.
RLS hoạt động ngầm bên dưới như thế nào?
Nhiều người lo ngại RLS sẽ làm chậm hệ thống. Thực tế, Postgres xử lý RLS bằng cách gộp biểu thức trong Policy vào mệnh đề WHERE của câu SQL gốc. Nó giống như một bộ lọc tự động và trong suốt đối với người dùng.
Hai thành phần cốt lõi của Policy
- USING: Kiểm soát các dòng dữ liệu hiện có (SELECT, DELETE, UPDATE). Nếu điều kiện sai, dòng đó đơn giản là bị bỏ qua hoặc không thể xóa/sửa.
- WITH CHECK: Kiểm soát dữ liệu mới được đưa vào (INSERT, UPDATE). Ví dụ: Ngăn
dev_tungtạo đơn hàng nhưng lại gánownerlà một người khác.
CREATE POLICY protect_inserts ON orders
FOR INSERT
WITH CHECK (owner = current_user);
Giải pháp RLS cho ứng dụng SaaS thực tế
Trong môi trường Production, ứng dụng thường dùng Connection Pool (như PgBouncer) và kết nối tới DB bằng một user duy nhất (ví dụ: app_user). Lúc này current_user không còn tác dụng. Giải pháp tối ưu là sử dụng Session Variables.
Triển khai với Session Variables
Bạn thay đổi Policy để nó đọc giá trị từ cấu hình phiên làm việc:
CREATE POLICY saas_tenant_policy ON orders
USING (tenant_id = current_setting('app.current_tenant')::int);
Trong mã nguồn ứng dụng, ngay sau khi mượn một kết nối từ pool, bạn cần thực thi lệnh gán biến:
-- Giả sử ID của khách hàng hiện tại là 101
SET app.current_tenant = 101;
SELECT * FROM orders; -- Chỉ lấy dữ liệu của khách hàng 101
Lưu ý: Hãy luôn đảm bảo biến này được xóa hoặc đặt lại sau mỗi request để tránh rò rỉ dữ liệu giữa các phiên làm việc khác nhau.
Kinh nghiệm thực chiến: Tránh các cạm bẫy về hiệu năng
Sau khi áp dụng RLS cho hệ thống quản lý kho với hàng triệu bản ghi, tôi đã rút ra được 3 bài học đắt giá:
1. Index là yếu tố sống còn
RLS thực chất là thêm WHERE. Nếu cột tenant_id không được đánh Index, Postgres sẽ phải quét toàn bộ bảng (Full Table Scan) cho mọi truy vấn. Trong một dự án cũ, việc thêm Index vào cột này đã giúp thời gian phản hồi giảm từ 800ms xuống còn chưa đầy 10ms.
2. Cảnh giác với quyền Superuser
Postgres mặc định bỏ qua RLS đối với tài khoản superuser hoặc chủ sở hữu bảng (owner). Nếu bạn thấy truy vấn vẫn trả về toàn bộ dữ liệu dù đã bật RLS, hãy kiểm tra lại quyền của user. Để bắt buộc áp dụng cho cả chủ sở hữu, hãy dùng:
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
3. Tránh Subquery phức tạp trong Policy
Hạn chế viết Policy kiểu USING (tenant_id IN (SELECT id FROM user_permissions ...)). Postgres có thể thực thi Subquery này lặp đi lặp lại cho từng dòng dữ liệu. Với bảng 1 triệu dòng, hiệu năng sẽ sụt giảm nghiêm trọng. Thay vào đó, hãy sử dụng thông tin có sẵn trong session hoặc kỹ thuật Join hiệu quả.
Tổng kết
RLS không phải là viên đạn bạc cho mọi vấn đề, nhưng nó là lớp phòng vệ chiều sâu (Defense in depth) cực kỳ mạnh mẽ. Nó tách biệt hoàn toàn logic bảo mật khỏi code ứng dụng. Với các hệ thống cần độ an toàn cao, áp dụng RLS sớm sẽ giúp bạn tránh được những lỗi rò rỉ dữ liệu nghiêm trọng có thể gây thiệt hại lớn về uy tín.

