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>
This commit is contained in:
199
scripts/db/rls-policies.sql
Normal file
199
scripts/db/rls-policies.sql
Normal file
@@ -0,0 +1,199 @@
|
||||
-- ============================================================================
|
||||
-- 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.
|
||||
-- ============================================================================
|
||||
Reference in New Issue
Block a user