# 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]]