[PostgreSQL] RLS로 테넌트별 행을 세션 변수 하나로 격리한다
설치·접속: PostgreSQL 설치와 접속
부제: 한 테이블에 여러 고객사(테넌트) 데이터가 섞여 있을 때, 애플리케이션이 WHERE를 빠뜨려도 남의 행이 절대 안 보이게 DB에서 강제할 때
CREATE TABLE m9_tenant_docs (id serial PRIMARY KEY, tenant text, body text);
INSERT INTO m9_tenant_docs (tenant, body) VALUES
('acme','acme 계약서'), ('acme','acme 매뉴얼'),
('globex','globex 보고서'), ('globex','globex 회의록');
ALTER TABLE m9_tenant_docs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_iso ON m9_tenant_docs
USING (tenant = current_setting('app.tenant', true));
GRANT SELECT, INSERT, UPDATE, DELETE ON m9_tenant_docs TO m9_app;
-- 앱유저로 전환 후, 세션 변수로 "지금 이 고객사"를 지정
SET ROLE m9_app;
SET app.tenant = 'acme';
SELECT * FROM m9_tenant_docs;
id | tenant | body
----+--------+-------------
1 | acme | acme 계약서
2 | acme | acme 매뉴얼
(2 rows)app.tenant를 globex로 바꾸면 같은 쿼리가 globex 행 2개만 반환한다. 테이블 데이터는 그대로인데, RLS 정책이 매 쿼리에 WHERE tenant = current_setting('app.tenant')를 자동으로 덧붙이는 효과다. 애플리케이션은 로그인 직후 세션 변수 하나만 세팅하면 되고, 이후 어떤 쿼리를 던지든 다른 테넌트 행은 물리적으로 결과에서 빠진다. 실수로 WHERE 조건을 빼먹어도 격리가 무너지지 않는 게 핵심이다. current_setting의 두 번째 인자 true는 변수가 없을 때 에러 대신 NULL을 돌려줘, 변수 미설정 시 아무 행도 안 보이게 안전하게 닫힌다.
이렇게도 쓴다
읽기뿐 아니라 INSERT도 막는다. 남의 테넌트로 데이터를 심으려 하면 거부. (조합: WITH CHECK)
CREATE POLICY tenant_iso ON m9_tenant_docs
USING (tenant = current_setting('app.tenant', true))
WITH CHECK (tenant = current_setting('app.tenant', true));
-- SET app.tenant='acme' 상태에서 globex 행 INSERT -> 정책 위반 에러
테이블 소유자도 정책을 우회 못 하게 강제한다. (조합: FORCE ROW LEVEL SECURITY)
ALTER TABLE m9_tenant_docs FORCE ROW LEVEL SECURITY;
동작(SELECT/INSERT/UPDATE/DELETE)별로 다른 정책을 건다. (조합: FOR 절)
CREATE POLICY read_all ON m9_tenant_docs FOR SELECT USING (true);
CREATE POLICY write_own ON m9_tenant_docs FOR INSERT
WITH CHECK (tenant = current_setting('app.tenant', true));
특정 관리자 롤에만 정책을 적용해 일반 롤은 전체를 보게 한다. (조합: TO 롤)
CREATE POLICY tenant_iso ON m9_tenant_docs TO m9_app
USING (tenant = current_setting('app.tenant', true));
정책이 어떻게 걸렸는지 확인한다. (조합: \d 상세)
SELECT polname, polcmd, pg_get_expr(polqual, polrelid) FROM pg_policy;
임시로 RLS를 꺼서 전체를 본다(소유자·슈퍼유저 점검용). (조합: DISABLE)
ALTER TABLE m9_tenant_docs DISABLE ROW LEVEL SECURITY;