feat: PostgreSQL RLS 기반 사업장 2차 격리 활성화 - #135
Draft
krestar wants to merge 3 commits into
Draft
Conversation
- #125의 V40/V41 다음 V42 migration으로 tenant 보호 테이블 38개의 RLS를 활성화 - migration의 lock timeout과 statement timeout을 설정 - 정책 테이블, RLS 활성 테이블, FORCE 미적용 상태를 schema 테스트로 검증 - RLS 환경에서 runtime timeout 테스트가 실제 tenant context로 UPDATE를 수행하도록 보정
krestar
marked this pull request as draft
August 11, 2026 05:10
12 tasks
Contributor
|
Migration 번호 조율 공유드립니다. 제가 진행 중인 담당자 변경 PR #143의 공통 Migration에서 V42를 사용하기로 했습니다. 이 PR의 PostgreSQL 전용 권장 병합 순서는 다음과 같습니다.
서로 다른 migration 경로라도 Flyway version은 함께 비교되므로 V42가 겹치지 않도록 조정이 필요합니다. |
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.
왜 필요한가요?
현재의
ActorContext + company_id 범위 Repository + tenant-aware DB 제약을 유지하면서,PostgreSQL이 다른 사업장의 행을 한 번 더 차단하도록 준비된 RLS 정책을 실제로 활성화합니다.
이 PR은 RLS가 애플리케이션 권한 검사나 Repository의
company_id조건을 대신하게 하지 않습니다.애플리케이션 코드에 범위 조건이 누락되더라도 runtime DB role에서 다른 사업장 행을 조회·생성·수정·삭제할 수 없도록 DB 차단 계층을 추가합니다.
mainmerge 즉시 배포와 수동 Smoke Test가 이어지므로, PR merge만으로 Issue를 자동 종료하지 않습니다.live 검증까지 끝난 후 #34를 수동으로 닫습니다.
무엇이 바뀌나요?
DB migration
V42__enable_postgresql_rls.sql을 추가합니다.ENABLE ROW LEVEL SECURITY를 적용합니다.FORCE ROW LEVEL SECURITY는 적용하지 않습니다.lock_timeout = 5sstatement_timeout = 30s테스트 계약
relrowsecurity = true인 테이블: 동일한 38개relforcerowsecurity = true인 테이블: 0개notificationschema 검증을 포함합니다.UPDATE 0 rows로 통과하는 false positive를 막습니다.1인지 검증합니다.영향받는 테이블
총 38개입니다.
company,company_settings,user_account,refresh_token,user_agreement_consent,password_reset_tokenworker,worker_document,stored_file,task,task_checklist_item,task_transition_history,document_request_draft,document_request_draft_type,approval_request,external_submission,task_evidence,audit_event,workflow_case,document_ocr_run,notificationworker_link,worker_response,worker_response_upload,worker_document_upload_idempotency,worker_import_job,worker_import_row,worker_import_commit_idempotencyai_run,ai_attempt,ai_question,ai_candidate,ai_candidate_decision_batch,ai_candidate_decision,ai_candidate_decision_taskevent_publication,event_consumption,outbox_manual_retry어떻게 검증했나요?
자동 테스트
PostgreSQL 16.14를 사용한 최신 로컬 test report 기준입니다.
실행 명령:
주요 PostgreSQL 검증 결과:
PostgreSqlMigrationTests: 1/1 통과PostgreSqlRuntimeTimeoutBehaviorIntegrationTest: 3/3 통과PostgreSqlRestrictedRoleHttpE2ETest: 7/7 통과PostgreSqlRlsIsolationTest: 1/1 통과PostgreSqlTenantDatabaseContextTest: 7/7 통과OutboxIntegrationTest: 6/6 통과AuthRefreshPostgreSqlConcurrencyTest: 1/1 통과restricted-role negative probe에서 기록되는 SQLSTATE
42501은 의도한 access-denied 결과입니다.수동 RLS 활성화 SQL 검증
아래 검증은 동일한 RLS 활성화 SQL이 임시 V40 번호였을 때 수행했습니다. Seed/RLS 충돌과 runtime 동작을 확인한
증거로는 유효하지만, 최종 V42 migration 순서를 증명하지는 않습니다. #125의 V40/V41 merge 후 새 DB에서
V39 → V40 → V41 → V42순서를 다시 검증합니다.fowoco_migration과fowoco_runtime을 분리해 생성했습니다.DEMO_SEED_ENABLED=true로 기존 Demo/Test fixture를 생성했습니다.38380EXECUTE권한을 runtime role에 부여했습니다.DEMO_SEED_ENABLED=true재기동이 아래 예상 원인으로 실패함을 확인했습니다.company행을 보지 못함companyinsert를 시도함WITH CHECK가 SQLSTATE42501로 차단함DEMO_SEED_ENABLED=false, outbox enabled 상태로 정상 기동했습니다.수동 runtime Smoke Test
/api/v1/auth/me성공/documents에서 Demo/Test 계정별 worker/task 격리 확인company조회가 0건임을 확인bootstrap_company_id_by_normalized_email,bootstrap_company_id_by_refresh_token_hash,bootstrap_claim_event_publications호출에 permission/RLS 오류가 없음을 확인EXECUTE = true확인42501, scheduled-task 오류가 없음을 확인현재 수동 로그의 outbox 검증은 빈 polling 경로까지 확인했습니다.
Merge 전 staging에서는 실제 event 1건의
claim → tenant context 설정 → handler → complete까지 추가로 확인합니다.별도 관찰 사항
GET /api/v1/notifications를 cursor 없이 호출할 때 PostgreSQL이 nullable cursor placeholder 타입을 추론하지 못하는SQLSTATE
42P18이 관찰됐습니다.RLS, bootstrap 권한, outbox와 독립적인 notification 조회 쿼리 문제이며 이 PR에서는 수정하지 않습니다.
Demo Seed 주의사항
이 PR은 Demo Seed 코드나 기존 fixture 데이터를 변경하지 않습니다.
현재 배포 환경은
DEMO_SEED_ENABLED=true이지만, 현재main의 Demo Seed runner는 RLS 활성 상태와 호환되지 않습니다.V42는 Flyway transaction에서 먼저 commit되고 그 후 seed runner가 실패하므로,
Pod가 기동하지 못해도 DB는 V42/RLS 활성 상태로 남습니다.
단순 image rollback으로 V42를 되돌릴 수 없습니다.
Seed를
false로 변경해도 기존 PostgreSQL PVC의 Demo/Test 데이터는 삭제되지 않습니다.다만 재기동 시 fixture를 자동 복구하거나 보충하지 않습니다.
따라서 merge 전에 아래 두 선택지 중 하나를 명시적으로 승인해야 합니다.
DEMO_SEED_ENABLED=true + 제한 runtime role + RLS enabled재기동을 별도 DB에서 검증합니다.server-env의DEMO_SEED_ENABLED=false를 적용합니다.false로 유지합니다.true로 바꾸지 않습니다.팀 요구사항이 “배포 환경에서 Demo Seed 활성 유지”라면 1번이 hard gate입니다.
2번을 선택하려면 Seed 비활성 유지 기간과 fixture 자동 복구가 없는 운영 제약을 담당자가 명시적으로 승인해야 합니다.
열린/최근 PR과 migration 순서
2026-08-11 확인 기준입니다.
main반영 필요task,document_request_draft쓰기 경로 회귀 테스트 필요#134 V39 → #125 V40/V41 → #34 V42로 고정합니다.main과 live DB에 적용되기 전에 #34를 merge하지 않습니다.main으로 rebase하고 전체 PostgreSQL 테스트와 notification/outbox 경로 Smoke Test를 반복합니다.Merge 조건
아래 항목을 모두 충족하기 전에는 merge하지 않습니다.
코드·migration
origin/main을 반영하고 docs: Swagger 실제 호출 준비와 프로젝트 문서 정리 #132/feat: 근로자 보안 링크 SMS 발송 연동 #134/#125가 포함됐는지 확인validate성공 및 pending migration 없음Seed
DEMO_SEED_ENABLED=true + RLS enabled재기동 성공false로 변경하고 기존 image 재기동 성공live DB preflight
flyway_schema_history의 V39, V40, V41 success와 V42 미적용 확인fowoco_runtime을 사용하는지 확인rolsuper=false,rolbypassrls=false,rolcreaterole=false,rolcreatedb=falseTRUNCATE,REFERENCES, DDL 권한이 없음EXECUTE=truerollout 준비
mainmerge 중지DISABLE ROW LEVEL SECURITY하는 V43 rollback patch 준비 및 리뷰배포 절차
server/.github/workflows/deploy.yml은mainpush마다 자동 배포합니다.따라서 merge가 곧 live DB migration 시작 버튼입니다.
1. Merge 전
mainmerge를 중지합니다.server-envSecret의 Seed 설정을 결정한 값으로 변경합니다.임시 비활성 선택 시 예시:
Secret 변경만으로 기존 Pod 환경변수는 바뀌지 않으므로 V42 merge 전에 반드시 기존 image Pod를 재시작합니다.
2. Merge 및 V42 적용
deployworkflow를 단독으로 모니터링합니다.mainmerge를 하지 않습니다.lock_timeout=5s또는statement_timeout=30s로 실패하면 원인을 확인한 뒤 별도 시간대에 재시도합니다.3. 배포 후 Smoke Test
자동 health check 외에 다음을 제한 runtime role로 확인합니다.
/health200/api/v1/auth/mepermission denied, SQLSTATE42501, RLS policy violation, scheduled-task 오류 없음4. Seed 재활성화
현재 Seed 코드로는 재활성화하지 않습니다.
DEMO_SEED_ENABLED=true + RLS enabled재기동을 통과한 경우에만true로 되돌립니다.true로 변경한 뒤에도 Secret 적용을 위해 Pod 재시작과 Demo/Test login Smoke Test가 필요합니다.장애 대응·rollback
DEMO_SEED_ENABLED=false가 실제 새 Pod에 반영됐는지 확인합니다.flyway clean, schema history 수동 수정,flyway repair로 되돌리지 않습니다.DISABLE ROW LEVEL SECURITY를 적용합니다.company_id범위와 tenant-aware 제약은 그대로 유지합니다.DISABLE ROW LEVEL SECURITY는 배포 담당자 승인과 실행 기록이 있는 최후 수단으로만 사용하고,이후 동일 상태를 표현하는 forward migration을 반드시 추가합니다.
보안·개인정보
company_id조건과 tenant-aware FK/UNIQUE를 유지API·DB·운영 영향