feat(security): PostgreSQL RLS tenant 격리 기반 구축 - #65
Merged
Conversation
- ActorContext의 companyId를 transaction-local DB context로 설정 - Worker·Task·Approval·Audit 업무 흐름에 tenant context 연결 - H2와 PostgreSQL 환경별 tenant context 구현 분리 - transaction 외부 접근과 PostgreSQL context 설정 검증 보강
- Login과 Refresh Token의 최소 tenant bootstrap 함수를 추가 - payload를 노출하지 않는 tenant-safe Outbox claim 함수를 추가 - 현재 14개 tenant 테이블의 RLS policy를 비활성 상태로 준비 - SECURITY DEFINER 함수와 policy 카탈로그 및 권한 경계를 검증
- Login과 Signup에서 email 기반 tenant bootstrap을 적용 - Refresh와 Logout에서 token hash 기반 tenant bootstrap을 적용 - PostgreSQL SECURITY DEFINER 함수와 H2 조회 adapter를 분리 - tenant 확정 후 기존 Repository와 token family lock을 수행
- Outbox claim 단계에서 tenant 좌표만 반환하도록 bootstrap adapter 적용 - handler, 완료 및 실패 transaction에 tenant context 연결 - EventPublication 조회를 event_id와 company_id 복합 조건으로 제한 - RLS 적용 후에도 backlog metric이 동작하도록 payload-free 집계 함수 추가 - 제한 runtime role의 claim 권한과 교차 tenant payload 차단 검증
제한된 runtime role로 RLS를 임시 활성화해 회사별 CRUD 차단과 transaction-local tenant context 비누수를 검증합니다.
V8에서 bootstrap 함수와 RLS policy가 생성된 상태를 반영하고, RLS는 아직 비활성 상태임을 명확히 합니다.
- PostgreSQL statement timestamp를 claim과 lease의 기준 시각으로 사용 - owner, lease, batch size, max attempts를 DB 함수 경계에서 검증 - H2/local Clock 동작과 제한 role PostgreSQL 회귀 테스트를 유지·보강
- PostgreSQL Outbox 시간 소스로 statement_timestamp()를 사용 - claim, handler, completion, failure의 lease 판단 기준을 DB 시각으로 통일 - 행 잠금 획득 후 현재 시각을 조회해 대기 시간에 따른 시각 오차를 방지 - 로컬 및 H2 환경에서는 기존 Clock 기반 시간 소스를 유지 - lease duration을 1ms 이상 1일 이하로 제한 - 공백 문자로만 구성되거나 128자를 초과하는 claim owner를 거부 - 잘못된 claim 인자의 SQLSTATE 22023을 검증 - skewed JVM Clock 환경에서도 Outbox lifecycle이 정상 동작하는 회귀 테스트를 추가
- PostgreSQL의 millisecond 단위 lease 계약과 Java 설정을 일치 - toMillis() 변환에서 나노초가 조용히 절삭되는 설정을 거부 - 1ms + 1ns 입력에 대한 회귀 테스트를 추가
Contributor
|
stored_file, document_request_draft/_type이 tenant table 목록에 반영된 것 |
chaeliki
reviewed
Jul 29, 2026
chaeliki
self-requested a review
July 29, 2026 07:29
chaeliki
approved these changes
Jul 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
왜 필요한가요?
PostgreSQL RLS를 안전하게 도입하려면 인증된 업무 트랜잭션에 tenant context를 전달하고, 로그인·refresh·Outbox처럼 일반 tenant context를 바로 적용할 수 없는 경계를 제한된 bootstrap 함수로 분리해야 합니다.
또한 실제 활성화 전에 제한된 runtime role에서 회사 간 접근 차단과 connection pool 비누수를 검증해야 합니다.
Outbox claim부터 handler 실행, 완료 및 실패 처리까지 lease 판단에 동일한 시간 기준을 사용해야 합니다.
PostgreSQL 환경에서는 JVM clock skew의 영향을 받지 않도록 Outbox lifecycle 전체의 기준 시각을 DB
statement_timestamp()로 통일합니다.무엇이 바뀌나요?
API·도메인·DB 변경:
V10__prepare_postgresql_rls.sqlmigration을 추가했습니다.PUBLIC실행 권한을 제거했습니다.statement_timestamp()로 claim 가능 여부와 lease 만료 시각을 계산하도록 변경했습니다.22023으로 거부합니다.권한·Workflow 변경:
company_id를 transaction-local tenant context로 설정합니다.company_id를 transaction-local tenant context로 설정하고 해당 회사 범위 안에서 처리하도록 변경했습니다.statement_timestamp()로 통일했습니다.Clock기반 시간 소스를 유지합니다.AI·외부 연동 변경:
문서·배포 변경:
어떻게 검증했나요?
./gradlew clean test./gradlew build/health와 Swagger UI 확인PostgreSQL 17 Docker 환경에서 다음 테스트를 개별 실행한 후 전체 테스트를 실행했습니다.
PostgreSqlMigrationTestsPostgreSqlTenantDatabaseContextTestPostgreSqlRlsIsolationTest.\gradlew.bat clean test결과:
BUILD SUCCESSFULPostgreSQL 전용 테스트는 다음 환경변수가 설정된 경우 실행됩니다.
POSTGRES_TEST_ENABLED=truePOSTGRES_TEST_URLPOSTGRES_TEST_USERNAMEPOSTGRES_TEST_PASSWORD테스트에서는 RLS를 임시 활성화하여 다음을 확인하고, 종료 후 다시 비활성 상태로 복구합니다.
company_id변경 차단22023및 publication 비변경 검증보안·개인정보
company_id범위를 검사합니다.request_id로 추적됩니다.Proposed로 작성했습니다.ADR-0004의 transaction-local tenant context, 제한된 bootstrap 경계 및 단계적 RLS 활성화 결정을 따릅니다.
이번 PR에서 정책과 격리 테스트는 준비했지만 운영 RLS는 아직 활성화하지 않으므로, 전체 사업장 접근 검사 항목은 최종 활성화 단계에서 완료합니다. 새로운 업무 상태 변경을 추가하지 않아 AuditLog 이벤트도 추가하지 않았습니다.
API·DB·운영 영향
.env.example에 적었습니다.API 및 Client 호환성 변경은 없습니다.
PostgreSQL 전용 V10 migration은 RLS policy와 bootstrap 함수만 준비하며
ENABLE ROW LEVEL SECURITY는 수행하지 않습니다. Flyway migration은 수정·삭제하지 않는 forward-only 원칙을 따릅니다.Outbox bootstrap claim 함수는 호출자가
now와lease_expires_at을 전달하던 방식에서lease_duration_millis만 전달하는 방식으로 변경했습니다.현재 시각과 lease 만료 시각은 PostgreSQL 내부에서 계산합니다.
외부 API 계약 변경은 없으며 애플리케이션 내부 DB 함수 계약만 변경됩니다.
실제 DB role 생성, 최소 권한 부여와 Secret 주입은 관련 배포 작업과 조율하고, 운영 RLS 활성화 및 Smoke Test는 후속 단계에서 진행합니다.
화면 또는 응답 예시
화면 및 API 응답 변경은 없습니다.
리뷰할 부분
hywznn: migration 폴더를 새로 만들면, https://fowoco.github.io/server/ 에서 인식을 못한 것 같아서 수정해 봤는데 괜찮으려나요chaeliki: feat: 통합 문서함·파일 업로드·문서 준비도 구현 #57 에서 작업하신 코드에서 변경한 부분 하단에 간략하게 요약 했습니다.(변경된 파일 자체는 많아요 ㅜㅜ)통합 문서함·준비도
DocumentController.java
companyId만 넘기던 방식에서 전체 ActorContext를 서비스에 전달하도록 변경
DocumentReadinessController.java
준비도 계산 서비스에 ActorContext 전달
DocumentService.java
조회 트랜잭션 시작 시 actor.companyId()를 tenant DB context에 설정
DocumentReadinessService.java
준비도 조회 전에 tenant context 설정
DocumentRequestDraftService.java
기존 ActorContext를 tenant context에도 사용
파일 업로드
FileService.java
업로드 트랜잭션에 tenant context 설정
근로자 서류와 파일 연결
WorkerDocumentController.java
companyId를 Command에 복사하지 않고 ActorContext를 서비스에 전달
WorkerDocumentCreateCommand.java
내부 companyId 필드 제거
tenant는 서비스가 actor.companyId()에서 결정
WorkerDocumentPatchCommand.java
내부 companyId 필드만 제거
WorkerDocumentService.java
모든 public transactional 메서드에 tenant context 설정