# 1) RLS가 뭐고 왜 쓰나 - **RLS = 행 단위 접근 제어.** 같은 테이블이라도 “누가 요청했는지”에 따라 볼 수 있는 행을 자동으로 필터링. - Supabase는 PostgREST로 요청을 **DB까지 그대로** 전달하므로, **DB 레벨**에서 막아야 안전해. - 기본 원칙: **RLS 켜면 기본 거부(DENY)** → 네가 허용하는 정책만 통과. # 2) Supabase에서 쓰는 주요 역할/함수 - **anon**: 로그인 안 된 공개 키. - **authenticated**: 로그인된 사용자(JWT 보유). - **service_role**: 서버에서만 사용(모든 RLS 우회 가능 → 절대 클라이언트에 노출 금지). - `auth.uid()` : 현재 JWT의 사용자 id(UUID). - `auth.role()` : ‘anon’ 또는 ‘authenticated’. - `is_authenticated()` : 로그인 여부 boolean. - `jwt()` / `request.jwt()` : JWT의 raw/claim 접근(고급). # 3) 정책(Policy)의 구조 - 정책은 **동사(SELECT/INSERT/UPDATE/DELETE)** 별로 만든다. - 두 가지 조건식: - `USING` : **읽기/수정/삭제 허용 조건**(행을 “통과”시킬 필터). - `WITH CHECK` : **삽입/수정 시 입력되는 새 행의 유효성 검사**(권한 범위 밖의 값 쓰기 방지). - **여러 정책은 OR로 결합**(하나라도 true면 허용). --- # 4) 10분 실습(Quickstart) ## 4.1 테이블 만들기 ```sql -- 유저별 TODO 테이블 예시 create table public.todos ( id uuid primary key default gen_random_uuid(), user_id uuid not null, -- 소유자 title text not null, is_done boolean not null default false, created_at timestamptz not null default now() ); -- 인덱스(권장) create index if not exists idx_todos_user on public.todos(user_id); ``` ## 4.2 RLS 켜기 ```sql alter table public.todos enable row level security; ``` ## 4.3 “본인 것만 보기/쓰기” 정책 ```sql -- SELECT: 본인이 소유한 행만 조회 허용 create policy "read own todos" on public.todos for select to authenticated using ( user_id = auth.uid() ); -- INSERT: 본인 소유로만 생성 허용 create policy "insert own todos" on public.todos for insert to authenticated with check ( user_id = auth.uid() ); -- UPDATE: 본인 소유만 수정 허용 create policy "update own todos" on public.todos for update to authenticated using ( user_id = auth.uid() ) with check ( user_id = auth.uid() ); -- DELETE: 본인 소유만 삭제 허용 create policy "delete own todos" on public.todos for delete to authenticated using ( user_id = auth.uid() ); ``` > 포인트 > > - **USING**은 “그 행을 만질 수 있냐” > > - **WITH CHECK**는 “그 값으로 바꿔도 되냐(삽입/수정 결과 검증)” > > - INSERT는 USING이 아닌 **WITH CHECK**로만 검증된다는 점 잊지 말기! > ## 4.4 비로그인(anon) 완전 차단(기본) - 위 정책은 `to authenticated`만 허용하므로 **anon은 자동 거부**. - 공개 목록이 필요하면 `to anon` 정책을 별도로 추가. --- # 5) 조직(organization) 멀티테넌트 패턴 ## 5.1 스키마 ```sql create table public.organizations ( id uuid primary key default gen_random_uuid(), name text not null ); create table public.memberships ( org_id uuid not null references public.organizations(id) on delete cascade, user_id uuid not null, role text not null check (role in ('owner','admin','member')), primary key (org_id, user_id) ); create table public.org_posts ( id uuid primary key default gen_random_uuid(), org_id uuid not null references public.organizations(id) on delete cascade, author_id uuid not null, title text not null, body text not null, created_at timestamptz not null default now() ); alter table public.memberships enable row level security; alter table public.org_posts enable row level security; ``` ## 5.2 정책: 같은 조직원만 접근 ```sql -- org_posts 읽기: 같은 조직 멤버면 OK create policy "org read" on public.org_posts for select to authenticated using ( exists ( select 1 from public.memberships m where m.org_id = org_posts.org_id and m.user_id = auth.uid() ) ); -- org_posts 쓰기: 해당 org 멤버만 생성 가능 create policy "org insert" on public.org_posts for insert to authenticated with check ( exists ( select 1 from public.memberships m where m.org_id = org_posts.org_id and m.user_id = auth.uid() ) ); -- 수정/삭제: 글쓴이이거나(or) 조직에서 admin 이상 create policy "org update" on public.org_posts for update to authenticated using ( author_id = auth.uid() or exists ( select 1 from public.memberships m where m.org_id = org_posts.org_id and m.user_id = auth.uid() and m.role in ('owner','admin') ) ) with check ( author_id = auth.uid() or exists ( select 1 from public.memberships m where m.org_id = org_posts.org_id and m.user_id = auth.uid() and m.role in ('owner','admin') ) ); create policy "org delete" on public.org_posts for delete to authenticated using ( author_id = auth.uid() or exists ( select 1 from public.memberships m where m.org_id = org_posts.org_id and m.user_id = auth.uid() and m.role in ('owner','admin') ) ); ``` --- # 6) 컬럼 단위 보호(패턴) - RLS는 “행” 단위라서, **민감컬럼은 뷰/함수로 숨기는** 게 일반적. - 예) 내부 관리용 `internal_note`는 일반 사용자에게 숨기고, 관리 뷰만 노출. ```sql -- 원본 테이블에 민감 컬럼 유지 create table public.users_private ( user_id uuid primary key, phone text, internal_note text ); alter table public.users_private enable row level security; -- 일반 사용자용 뷰(민감 컬럼 제외) create view public.users_public as select user_id, phone from public.users_private; -- 뷰에 대한 SELECT만 허용 create policy "public view self" on public.users_public for select to authenticated using ( user_id = auth.uid() ); ``` > 직접 UPDATE 필요하면 **SECURITY DEFINER 함수**(RPC)로 안전하게 허용하는 패턴도 자주 씀. --- # 7) Storage(파일) 정책 간단 예시 Storage도 테이블 기반이라 RLS 원리 동일. ```sql -- 예: users/{user_id}/... 경로만 본인 접근 허용 create policy "read own files" on storage.objects for select to authenticated using ( bucket_id = 'users' and (storage.foldername(name))[1] = auth.uid()::text ); create policy "write own files" on storage.objects for insert to authenticated with check ( bucket_id = 'users' and (storage.foldername(name))[1] = auth.uid()::text ); ``` > 경로 파싱 유틸은 프로젝트 버전에 따라 다를 수 있어. 보통 `name like auth.uid()::text || '/%'` 같은 단순 조건을 자주 사용. --- # 8) 테스트 & 디버깅 팁 - **테스트 방법** 1. Supabase SQL Editor → “**Run as**”에서 `authenticated`/`anon` 선택 가능(프로젝트 UI에 기능이 없다면 JWT로 psql 테스트). 2. 클라이언트에서 실제 로그인 후 요청(가장 현실적). - **자주 나는 문제** - RLS 켰는데 데이터가 “사라짐” → **정책이 없으면 전부 거부**가 정상. 정책을 추가해야 보여. - INSERT가 실패 → `WITH CHECK` 조건 불일치. 보통 `user_id`를 클라이언트가 잘못 보냄(또는 안 보냄). **서버에서 `user_id := auth.uid()`로 강제 세팅**하는 트리거나 RPC 고려. - 여러 정책이 있을 때 **OR 합성**이라는 걸 잊음 → 의도보다 열릴 수 있음. 정책을 **가능한 구체적으로** 쓰자. - **service_role** 키로 테스트해놓고 “왜 앱에서 막히지?” → service_role은 RLS 우회. **클라이언트 키**로 테스트해야 실제 동작 확인 가능. --- # 9) 프로덕션 체크리스트 - 모든 테이블 **RLS 활성화** 확인. - 최소 권한 원칙: `to authenticated`/`to anon` 정확히 구분. - INSERT/UPDATE에 **WITH CHECK** 누락 없는지. - 멀티테넌트: **org 경유 EXISTS** 패턴으로 테넌트 격리. - 민감 데이터는 **뷰/함수**로 노출 축소. - 인덱스: 정책에서 조인/필터 쓰면 적절히 추가(예: `memberships(org_id, user_id)`). - service_role 키는 **서버 전용** 저장소(환경변수)에서만 사용. --- # 10) 너의 테이블(bio_stocks) 적용 예시(읽기 전용 공개 or 제한) 예: 로그인 사용자만 자신의 관심 목록(관심 테이블 별도)과 조인해 접근 허용. ```sql -- 관심종목 테이블 create table public.watchlists ( user_id uuid not null, symbol text not null, primary key (user_id, symbol) ); alter table public.watchlists enable row level security; -- 본인만 자신의 watchlist 조회 가능 create policy "watchlist self read" on public.watchlists for select to authenticated using ( user_id = auth.uid() ); create policy "watchlist self upsert" on public.watchlists for insert to authenticated with check ( user_id = auth.uid() ); create policy "watchlist self delete" on public.watchlists for delete to authenticated using ( user_id = auth.uid() ); -- bio_stocks는 전 세계 공용 데이터라면 공개 조회 허용(선택) alter table public.bio_stocks enable row level security; create policy "public stock read" on public.bio_stocks for select to anon, authenticated using ( true ); ``` > 만약 **bio_stocks를 비공개로** 운영하고 특정 조직만 보게 하려면, 위의 조직 패턴으로 `org_id`를 테이블에 추가하고 `memberships` 기반 EXISTS 정책으로 제한하면 돼. --- # 11) 다음 단계(심화) - **행 수준 + 컬럼 마스킹**: 뷰/함수 조합으로 세밀한 마스킹. - **정책 재사용**: `create policy … using (my_can_access_row(…))` 형태로 **함수화**하면 중복 제거/테스트 용이. - **실시간(realtime)**: RLS가 pub/sub에도 적용됨(구독 전파도 정책에 의해 필터됨). - **감사 로그**: 트리거로 변경 이력 테이블에 insert(단, service_role 우회 트래픽 포함 설계 주의). --- 관련 문서: [[Supabase]], [[postgreSQL]]