빌드 안내 · 주석을 이 저장소 기준으로 정리
- 빌드 안내(BUILD.md)의 JDK 경로를 설치 위치와 무관한 지정 방식으로 변경 - 5탭 계약 파일 탐색 순서를 이 저장소 구조(app/design → 루트 design)로 정리 - README · 보안 규칙 · 소스 주석에서 이 저장소에 없는 문서 경로를 문서 이름으로 바꾸거나 삭제 - Flutter 버전 고정 파일(app/.fvmrc, 3.47.2) 추가, README 필수 환경을 같은 버전으로 갱신 - 동작 변경 없음
This commit is contained in:
@@ -3,8 +3,8 @@ rules_version = '2';
|
||||
// FANUZU — Firestore 보안 규칙 (2주차 설계본 v0.1)
|
||||
//
|
||||
// 근거
|
||||
// - wireframe/handoff/rbac-summary.md 역할 3종 · 운영자 등급 3단계(D-18)
|
||||
// - Docs/별첨1_기능별_완료기준표.html 「전 기능 공통 완료 기준 — 서버검증 원칙」
|
||||
// - 요구사항 정의서 권한 요구 역할 3종 · 운영자 등급 3단계
|
||||
// - 별첨 1 기능별 완료 기준표 「전 기능 공통 완료 기준 — 서버검증 원칙」
|
||||
// - 1주차 정책 설계서 P-02 원장 무결성 · P-06 어뷰징 방어
|
||||
//
|
||||
// 3대 원칙
|
||||
@@ -48,15 +48,15 @@ service cloud.firestore {
|
||||
function myUser() {
|
||||
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
|
||||
}
|
||||
// 작성자 표시명이 프로필 원본과 같은지 — 운영팀 사칭 차단 (교차검증 F-05)
|
||||
// 작성자 표시명이 프로필 원본과 같은지 — 운영팀 사칭 차단
|
||||
function authorNameMatchesProfile() {
|
||||
return request.resource.data.authorName == myUser().displayName;
|
||||
}
|
||||
// 소속 팬덤이 본인 소속과 같은지 — 타 팬덤 피드 삽입 차단 (교차검증 F-05)
|
||||
// 소속 팬덤이 본인 소속과 같은지 — 타 팬덤 피드 삽입 차단
|
||||
function fandomIsMine() {
|
||||
return request.resource.data.fandomId == myUser().fandomId;
|
||||
}
|
||||
// 무인증 열람을 허용하는 캠페인 상태 — draft·review 는 제외 (교차검증 F-08)
|
||||
// 무인증 열람을 허용하는 캠페인 상태 — draft·review 는 제외
|
||||
function campaignIsPublic() {
|
||||
return resource.data.status in ['funding', 'success', 'fail', 'report'];
|
||||
}
|
||||
@@ -97,7 +97,7 @@ service cloud.firestore {
|
||||
match /users/{uid} {
|
||||
allow get: if isSelf(uid) || isViewer();
|
||||
allow list: if isViewer(); // 전체 사용자 목록은 운영자만(S-AD-USER)
|
||||
// 교차검증 F-06 — 클라이언트 생성 경로를 닫는다.
|
||||
// 클라이언트 생성 경로를 닫는다.
|
||||
// 기존에는 클라이언트 생성도 허용해 ① fn-auth-oncreate(§8.5)와 생성 경합이 생기고
|
||||
// ② 클라이언트가 먼저 만들면 totalContributedPop 등 서버 산출 필드가 빈
|
||||
// 사용자 문서가 남았다(별첨1 조건부적합 제외 ④ 로그인 불가와 닿는 지점).
|
||||
@@ -150,7 +150,7 @@ service cloud.firestore {
|
||||
// 보유 배지 이력. 부여는 서버 판정.
|
||||
// 화면: S-M-BADGE(본인 도감) · S-M-IDCARD(본인 카드)
|
||||
//
|
||||
// 교차검증 G-04 — 예전 주석은 「타인 프로필의 배지 노출 허용」이라 적고 signedIn() 으로 열어 뒀는데,
|
||||
// 예전 주석은 「타인 프로필의 배지 노출 허용」이라 적고 signedIn() 으로 열어 뒀는데,
|
||||
// **1차 범위에 타인 프로필 화면이 없다**(화면 95개 전수 확인). 주석이 사실과 달랐다.
|
||||
// 타인 배지가 실제로 보이는 곳은 S-M-HERO 히어로 랭킹 하나뿐이고,
|
||||
// 그것은 /fandoms/{f}/members/{uid}.honorBadgeIds 비정규화 필드로 읽는다(설계서 §2.3·§4.4).
|
||||
@@ -179,7 +179,7 @@ service cloud.firestore {
|
||||
// 본인은 자기 동의 이력을 볼 수 있어야 한다(열람권). 화면: S-A-TERMS · S-M-SETTINGS
|
||||
match /users/{uid}/consents/{consentId} {
|
||||
allow read: if isSelf(uid) || isViewer();
|
||||
allow write: if false; // 기록은 fn-consent-record 경유 (교차검증 F-04)
|
||||
allow write: if false; // 기록은 fn-consent-record 경유
|
||||
}
|
||||
|
||||
// 차단 목록. 본인만 읽고 쓴다.
|
||||
@@ -315,19 +315,19 @@ service cloud.firestore {
|
||||
// 캠페인. 목표 달성률·후원 합계는 집계 필드라 클라이언트가 못 쓴다.
|
||||
// 화면: S-C-LIST · S-C-DETAIL · S-C-NEW · S-AD-CAMPAIGN
|
||||
//
|
||||
// ⚠️ 목록 쿼리에는 status 필터가 필수다 (교차검증 G-03).
|
||||
// ⚠️ 목록 쿼리에는 status 필터가 필수다.
|
||||
// 아래 read 규칙이 공개 상태만 허용하므로, status 필터가 없는 목록 쿼리는
|
||||
// draft·review 문서까지 매칭돼 **쿼리 전체가 거부**된다(Firestore 는 list 규칙을
|
||||
// 반환 후보 전체에 적용한다). 인덱스 문제로 오인하기 쉬운 지점이다.
|
||||
// 허용되는 목록 쿼리 형태는 설계서 §6.1 IDX-15·16·17·19 참조.
|
||||
match /campaigns/{campaignId} {
|
||||
// 교차검증 F-08 — 예전에는 read: if true 라 심사 중(review)·초안(draft) 캠페인 본문이
|
||||
// 예전에는 read: if true 라 심사 중(review)·초안(draft) 캠페인 본문이
|
||||
// URL만 알면 미로그인 상태로 읽혔다. FR-CMP-01「승인 대기 캠페인은 목록에 노출하지 않는다」(E-CMP-03)를
|
||||
// 클라이언트 쿼리 필터가 아니라 규칙에서 강제한다. 공개 상태만 무인증 열람 대상이다.
|
||||
allow read: if campaignIsPublic()
|
||||
|| isOps()
|
||||
|| (signedIn() && resource.data.creatorUid == request.auth.uid);
|
||||
// 교차검증 F-02 — 화이트리스트 방식. 목록에 없는 필드가 하나라도 있으면 거부되므로
|
||||
// 화이트리스트 방식. 목록에 없는 필드가 하나라도 있으면 거부되므로
|
||||
// 나중에 집계 필드가 추가돼도 자동으로 막힌다. 집계·분류 필드는 초기값을 못 벗어난다.
|
||||
allow create: if signedIn()
|
||||
&& request.resource.data.keys().hasOnly([
|
||||
@@ -367,7 +367,7 @@ service cloud.firestore {
|
||||
// 화면: S-C-RESULT
|
||||
match /campaigns/{campaignId}/reports/{reportId} {
|
||||
allow read: if true;
|
||||
// 교차검증 F-07 — §7.3 표는 「성공 캠페인 개설자」라고 적었는데 규칙은 creatorUid만 보고
|
||||
// §7.3 표는 「성공 캠페인 개설자」라고 적었는데 규칙은 creatorUid만 보고
|
||||
// 상태를 확인하지 않아 실패·초안 캠페인에도 결과보고를 올릴 수 있었다.
|
||||
// 별첨1 ⑤ 판정 기준 「결과보고 게시 확인」의 대상은 성공한 캠페인이다.
|
||||
allow create: if signedIn()
|
||||
@@ -385,7 +385,7 @@ service cloud.firestore {
|
||||
// 화면: S-I-LOUNGE · S-I-POST · S-I-POST-NEW
|
||||
match /posts/{postId} {
|
||||
allow read: if signedIn();
|
||||
// 교차검증 F-05 — 예전에는 authorUid·likeCount·visibility 3개만 봐서
|
||||
// 예전에는 authorUid·likeCount·visibility 3개만 봐서
|
||||
// ① authorName 을 "FANUZU 운영팀"으로 적어 사칭 ② commentCount 위조
|
||||
// ③ 소속하지 않은 팬덤 피드에 글 삽입이 가능했다. 화이트리스트 + 원본 대조로 닫는다.
|
||||
allow create: if authorIsSelf()
|
||||
@@ -398,7 +398,7 @@ service cloud.firestore {
|
||||
&& request.resource.data.likeCount == 0
|
||||
&& request.resource.data.commentCount == 0
|
||||
&& request.resource.data.visibility == 'visible';
|
||||
// 교차검증 F-14 — 'title' 을 뺐다. 설계서 §2.6 posts 스키마에 title 필드가 없다.
|
||||
// 'title' 을 뺐다. 설계서 §2.6 posts 스키마에 title 필드가 없다.
|
||||
allow update: if signedIn() && resource.data.authorUid == request.auth.uid
|
||||
&& touchesOnly(['body','imageUrls','updatedAt']);
|
||||
// 숨김·복원. rbac-summary 「콘텐츠 숨김·복원」 = 운영 이상
|
||||
@@ -416,7 +416,7 @@ service cloud.firestore {
|
||||
// 댓글. 화면: S-I-POST
|
||||
match /posts/{postId}/comments/{commentId} {
|
||||
allow read: if signedIn();
|
||||
// 교차검증 F-05 — 댓글에도 같은 표시명 사칭 경로가 있었다.
|
||||
// 댓글에도 같은 표시명 사칭 경로가 있었다.
|
||||
allow create: if authorIsSelf()
|
||||
&& request.resource.data.keys().hasOnly([
|
||||
'authorUid','authorName','body','visibility','createdAt','updatedAt'
|
||||
@@ -440,7 +440,7 @@ service cloud.firestore {
|
||||
// 화면: S-I-QUESTION · S-I-ANSWER-NEW
|
||||
match /questions/{questionId}/answers/{answerId} {
|
||||
allow read: if signedIn();
|
||||
// 교차검증 F-05 — 답변에도 같은 표시명 사칭 경로가 있었다.
|
||||
// 답변에도 같은 표시명 사칭 경로가 있었다.
|
||||
allow create: if authorIsSelf()
|
||||
&& request.resource.data.keys().hasOnly([
|
||||
'authorUid','authorName','body','sourceUrls','accepted',
|
||||
@@ -505,7 +505,7 @@ service cloud.firestore {
|
||||
}
|
||||
|
||||
// 푸시 발송 이력·도달률. 운영자만 보고, 발송 집계는 Functions(FCM)가 쓴다.
|
||||
// 화면: S-AD-PUSH · rbac-summary 「푸시 발송 관리」 = 운영 이상 (교차검증 F-04 #2)
|
||||
// 화면: S-AD-PUSH · rbac-summary 「푸시 발송 관리」 = 운영 이상
|
||||
match /pushCampaigns/{pushId} {
|
||||
allow read: if isViewer();
|
||||
allow write: if false; // 발송·집계는 fn-push-send 경유
|
||||
@@ -579,7 +579,7 @@ service cloud.firestore {
|
||||
allow write: if false;
|
||||
}
|
||||
|
||||
// ─────────────── 12. 컬렉션 그룹 쿼리 (교차검증 F-01) ───────────────
|
||||
// ─────────────── 12. 컬렉션 그룹 쿼리 ───────────────
|
||||
//
|
||||
// Firestore에서 `match /users/{uid}/popTransactions/{txId}` 형태의 규칙은
|
||||
// **단일 컬렉션 쿼리에만** 적용되고 collectionGroup('popTransactions') 에는 적용되지 않는다.
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ERR, fail } from '../core/errors.js';
|
||||
/**
|
||||
* `fn-auth-oncreate` — 계정 문서의 유일한 생성 주체.
|
||||
*
|
||||
* 보안 규칙이 `users` 의 클라이언트 `create` 를 닫아 두었으므로(교차검증 F-06)
|
||||
* 보안 규칙이 `users` 의 클라이언트 `create` 를 닫아 두었으므로
|
||||
* 이 트리거가 만들지 않으면 계정 문서가 존재하지 않는다. 생성 주체를 하나로
|
||||
* 고정한 이유는 경합 때문이다 — 클라이언트가 먼저 만들면 서버 산출 필드가 빈
|
||||
* 사용자 문서가 남는다.
|
||||
@@ -49,7 +49,7 @@ export const fnAuthOnCreate = functionsV1.auth.user().onCreate(async (user) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* `fn-consent-record` — 약관 동의 이력(교차검증 F-04).
|
||||
* `fn-consent-record` — 약관 동의 이력.
|
||||
*
|
||||
* append-only 다. 철회도 기존 행을 고치지 않고 `granted: false` 인 새 행으로 남긴다.
|
||||
* 동의 시점을 되짚을 수 없으면 분쟁에서 증명할 수단이 사라진다.
|
||||
|
||||
Reference in New Issue
Block a user