Files
pos-system/scripts/db/rls-policies.sql
Ho Ngoc Hai 6061164873 feat: add multi-tenant row-level security across 5 services and 96 FnB engine unit tests
Security (P0-5):
- Implement ITenantProvider + HttpContextTenantProvider per service (order, fnb, inventory, catalog, wallet)
- Add EF Core global query filters for tenant isolation (shop_id/user_id based)
- Add TenantMiddleware setting PostgreSQL session variables for RLS
- Create PostgreSQL RLS policies script (scripts/db/rls-policies.sql)
- Adapter pattern bridges API-layer to Infrastructure-layer (Clean Architecture)
- Bypass mechanisms for admin roles, service-to-service calls, and migrations

Testing (P1-12):
- Add 96 unit tests for fnb-engine (up from 3)
- 57 domain entity tests: Table(18), KitchenTicket(12), Session(8), Reservation(13), Recipe(6)
- 39 command handler tests: CRUD operations, status transitions, validation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 13:40:34 +07:00

200 lines
7.6 KiB
PL/PgSQL

-- ============================================================================
-- EN: PostgreSQL Row-Level Security (RLS) Policies for Multi-Tenant Isolation
-- VI: PostgreSQL Row-Level Security (RLS) Policies cho Cách Ly Đa Tenant
-- ============================================================================
--
-- EN: These policies provide defense-in-depth for tenant isolation.
-- Primary mechanism: EF Core global query filters (application level).
-- Secondary mechanism: PostgreSQL RLS (database level).
--
-- The application sets session variables via:
-- SET LOCAL app.current_shop_id = '<uuid>';
-- SET LOCAL app.current_merchant_id = '<uuid>';
--
-- RLS policies use current_setting() to read these session variables.
-- When no session variable is set, the policy defaults to allowing access
-- (to support migrations, health checks, and admin operations).
--
-- VI: Các policies này cung cấp phòng thủ theo chiều sâu cho cách ly tenant.
-- Cơ chế chính: EF Core global query filters (cấp ứng dụng).
-- Cơ chế phụ: PostgreSQL RLS (cấp cơ sở dữ liệu).
-- ============================================================================
-- EN: Helper function to safely get current_setting with fallback
-- VI: Hàm trợ giúp để lấy current_setting an toàn với giá trị mặc định
CREATE OR REPLACE FUNCTION get_current_shop_id() RETURNS uuid AS $$
BEGIN
RETURN current_setting('app.current_shop_id', true)::uuid;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;
CREATE OR REPLACE FUNCTION get_current_merchant_id() RETURNS uuid AS $$
BEGIN
RETURN current_setting('app.current_merchant_id', true)::uuid;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;
CREATE OR REPLACE FUNCTION get_current_user_id() RETURNS uuid AS $$
BEGIN
RETURN current_setting('app.current_user_id', true)::uuid;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql STABLE;
-- ============================================================================
-- ORDER SERVICE DATABASE (order_service_db)
-- ============================================================================
-- EN: Connect to order service database before running these
-- VI: Kết nối đến database order service trước khi chạy các lệnh này
-- \connect order_service_db;
-- EN: Enable RLS on orders table
-- VI: Bật RLS trên bảng orders
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- EN: Policy: users can only see orders belonging to their shop
-- VI: Policy: users chỉ thấy orders thuộc shop của họ
CREATE POLICY tenant_isolation_orders ON orders
USING (
get_current_shop_id() IS NULL -- EN: No tenant set = allow (admin/migration)
OR shop_id = get_current_shop_id()
);
-- EN: Enable RLS on order_items table
-- VI: Bật RLS trên bảng order_items
ALTER TABLE order_items ENABLE ROW LEVEL SECURITY;
-- EN: Policy: order_items are isolated via their parent order's shop_id
-- VI: Policy: order_items được cách ly qua shop_id của order cha
CREATE POLICY tenant_isolation_order_items ON order_items
USING (
get_current_shop_id() IS NULL
OR order_id IN (SELECT id FROM orders WHERE shop_id = get_current_shop_id())
);
-- ============================================================================
-- FNB ENGINE DATABASE (fnb_engine_db)
-- ============================================================================
-- \connect fnb_engine_db;
-- EN: Tables
ALTER TABLE tables ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_tables ON tables
USING (
get_current_shop_id() IS NULL
OR shop_id = get_current_shop_id()
);
-- EN: Sessions
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_sessions ON sessions
USING (
get_current_shop_id() IS NULL
OR shop_id = get_current_shop_id()
);
-- EN: Reservations
ALTER TABLE reservations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_reservations ON reservations
USING (
get_current_shop_id() IS NULL
OR shop_id = get_current_shop_id()
);
-- EN: Kitchen tickets (isolated via session's shop)
ALTER TABLE kitchen_tickets ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_kitchen_tickets ON kitchen_tickets
USING (
get_current_shop_id() IS NULL
OR session_id IN (SELECT id FROM sessions WHERE shop_id = get_current_shop_id())
);
-- ============================================================================
-- INVENTORY SERVICE DATABASE (inventory_service_db)
-- ============================================================================
-- \connect inventory_service_db;
ALTER TABLE inventory_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_inventory_items ON inventory_items
USING (
get_current_shop_id() IS NULL
OR shop_id = get_current_shop_id()
);
-- EN: Inventory transactions are isolated via their parent item's shop
-- VI: Inventory transactions được cách ly qua shop của item cha
ALTER TABLE inventory_transactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_inventory_transactions ON inventory_transactions
USING (
get_current_shop_id() IS NULL
OR inventory_item_id IN (SELECT id FROM inventory_items WHERE shop_id = get_current_shop_id())
);
-- ============================================================================
-- CATALOG SERVICE DATABASE (catalog_service_db)
-- ============================================================================
-- \connect catalog_service_db;
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_products ON products
USING (
get_current_shop_id() IS NULL
OR shop_id = get_current_shop_id()
);
ALTER TABLE categories ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_categories ON categories
USING (
get_current_shop_id() IS NULL
OR shop_id = get_current_shop_id()
);
-- ============================================================================
-- WALLET SERVICE DATABASE (wallet_service_db)
-- ============================================================================
-- EN: Wallet uses user_id as tenant key (not shop_id)
-- VI: Wallet sử dụng user_id làm tenant key (không phải shop_id)
-- \connect wallet_service_db;
ALTER TABLE wallets ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_wallets ON wallets
USING (
get_current_user_id() IS NULL
OR user_id = get_current_user_id()
);
-- EN: Payments don't have direct user_id, accessed via wallet or order
-- VI: Payments không có user_id trực tiếp, truy cập qua wallet hoặc order
-- ============================================================================
-- EN: IMPORTANT NOTES
-- ============================================================================
-- 1. RLS policies apply to the database user running queries.
-- The superuser (postgres) bypasses RLS by default.
-- Use ALTER ROLE <app_user> NOBYPASSRLS; for the application user.
--
-- 2. To bypass RLS for migrations and admin operations:
-- - Run as superuser (postgres), OR
-- - Don't set app.current_shop_id session variable (policy allows NULL)
--
-- 3. To test RLS:
-- SET LOCAL app.current_shop_id = '<shop-uuid>';
-- SELECT * FROM orders; -- Should only return rows for that shop
--
-- 4. These helper functions need to be created in EACH service database.
-- Run the CREATE FUNCTION statements per database.
-- ============================================================================