한 줄 요약
현재의 ActorContext + company_id 범위 Repository + DB 제약을 유지하면서, PostgreSQL이 다른 사업장의 행을 한 번 더 차단하도록 RLS(Row Level Security)를 안전하게 도입합니다.
이 작업은 현재 MVP 구현을 막지 않는 P1 보안 고도화입니다. RLS를 성급하게 켜서 로그인과 공개 링크가 중단되지 않도록 선행조건을 먼저 갖춥니다.
쉽게 설명하면
지금도 서버는 사용자의 사업장을 확인하고 해당 사업장 데이터만 조회합니다. RLS는 서버 코드에 실수가 생겨도 PostgreSQL이 다른 사업장 데이터의 조회·저장을 한 번 더 막아주는 DB의 두 번째 자물쇠입니다.
JWT 또는 보안 링크
→ Server가 신뢰할 수 있는 company_id 결정
→ Transaction에 company_id 설정
→ Repository의 company_id 조건
→ PostgreSQL RLS가 같은 company_id 행만 허용
→ FK·UNIQUE·CHECK가 교차 사업장 연결 차단
RLS가 기존 권한 검사와 Repository 범위 조건을 대신하지는 않습니다.
배경
PR #32에서는 company, user_account, worker, task, ticket, document에 RLS 정책을 먼저 추가했지만, Spring Boot가 transaction마다 tenant context를 설정하는 코드와 로그인·Refresh Token·Worker Link의 bootstrap 흐름이 없었습니다.
또한 공식 식별자인 company_id 대신 company_name을 tenant UUID처럼 사용했고, local/test에서는 flyway.target: 1로 migration 자체를 건너뛰고 있었습니다. 이 상태로 운영하면 모든 조회가 막히거나 connection pool에서 잘못된 tenant context가 사용될 위험이 있었습니다.
PR #33에서는 MVP 기준선을 복구하고, ADR-0002에 아래 RLS 도입 조건을 기록했습니다. 이 이슈는 그 후속 구현을 담당합니다.
시작 전 확정할 결정
구현 범위
1. DB Role 분리
2. Transaction tenant context
3. Bootstrap 흐름
4. RLS 정책
5. Connection pool 안전성
6. 관측과 배포
초보자용 구현 순서
- RLS를 바로 켜지 말고 로그인·Refresh Token·Worker Link의 bootstrap 흐름을 먼저 그림으로 정리합니다.
- PostgreSQL에 migration용 계정과 실행용 계정을 각각 만듭니다.
- Spring transaction 안에서만
SET LOCAL app.company_id가 실행되는 작은 adapter를 구현합니다.
- 기존
company, user_account, refresh_token부터 staging 정책을 적용합니다.
- A 사업장과 B 사업장 데이터로 조회·생성·수정·삭제 차단 테스트를 작성합니다.
- connection을 반복 재사용하는 누수 테스트를 작성합니다.
- Worker·Task·Document는 각 테이블이 구현될 때 같은 규칙으로 정책을 확장합니다.
- Smoke Test와 롤백 연습 후에만 운영 또는 데모 PostgreSQL에 적용합니다.
완료 조건
이번 이슈에서 하지 않는 것
- Application Service의 역할·사업장 권한 검사 제거
- Repository의
company_id 조건 제거
- Worker·Task·Document 테이블 선행 생성
- AI 또는 Knowledge 저장소의 DB 권한 설계
- PostgreSQL superuser credential의 애플리케이션 사용
선행·연결 관계
한 줄 요약
현재의
ActorContext + company_id 범위 Repository + DB 제약을 유지하면서, PostgreSQL이 다른 사업장의 행을 한 번 더 차단하도록 RLS(Row Level Security)를 안전하게 도입합니다.쉽게 설명하면
지금도 서버는 사용자의 사업장을 확인하고 해당 사업장 데이터만 조회합니다. RLS는 서버 코드에 실수가 생겨도 PostgreSQL이 다른 사업장 데이터의 조회·저장을 한 번 더 막아주는 DB의 두 번째 자물쇠입니다.
RLS가 기존 권한 검사와 Repository 범위 조건을 대신하지는 않습니다.
배경
PR #32에서는
company,user_account,worker,task,ticket,document에 RLS 정책을 먼저 추가했지만, Spring Boot가 transaction마다 tenant context를 설정하는 코드와 로그인·Refresh Token·Worker Link의 bootstrap 흐름이 없었습니다.또한 공식 식별자인
company_id대신company_name을 tenant UUID처럼 사용했고, local/test에서는flyway.target: 1로 migration 자체를 건너뛰고 있었습니다. 이 상태로 운영하면 모든 조회가 막히거나 connection pool에서 잘못된 tenant context가 사용될 위험이 있었습니다.PR #33에서는 MVP 기준선을 복구하고, ADR-0002에 아래 RLS 도입 조건을 기록했습니다. 이 이슈는 그 후속 구현을 담당합니다.
시작 전 확정할 결정
Proposed → Accepted처리합니다.SUPERUSER,BYPASSRLS, table owner 권한을 갖지 않습니다.구현 범위
1. DB Role 분리
.env.example에는 변수 이름만 기록하고 credential은 저장하지 않습니다.2. Transaction tenant context
ActorContext.companyId를 신뢰 원본으로 사용합니다.SET LOCAL app.company_id = ...를 실행합니다.company_id를 tenant context로 신뢰하지 않습니다.3. Bootstrap 흐름
company_id를 확정할 수 있어야 합니다.company_id를 확정합니다.company_id만 확정합니다.4. RLS 정책
SELECT,INSERT,UPDATE,DELETE모두 같은 사업장 행만 허용합니다.5. Connection pool 안전성
6. 관측과 배포
request_id로 추적합니다.초보자용 구현 순서
SET LOCAL app.company_id가 실행되는 작은 adapter를 구현합니다.company,user_account,refresh_token부터 staging 정책을 적용합니다.완료 조건
이번 이슈에서 하지 않는 것
company_id조건 제거선행·연결 관계