Repository navigation
Expand file tree
/
Copy pathfirestore.rules
More file actions
81 lines (74 loc) · 4.57 KB
/
Copy pathfirestore.rules
File metadata and controls
81 lines (74 loc) · 4.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /todos/{todoId} {
// description은 선택 필드다. 없거나 null이면 그대로 통과시키고, 값이 있으면
// 문자열이면서 길이 상한 이내여야 한다.
// 상한 2000은 클라이언트의 DESCRIPTION_MAX_LENGTH(shared/utils/descriptionLinks.ts)와
// 같은 값이어야 한다 — 한쪽만 바꾸면 클라이언트는 통과시키는데 서버가 거부한다.
function descriptionValid() {
let d = request.resource.data.get("description", null);
return d == null || (d is string && d.size() <= 2000);
}
// update의 request.resource.data는 "쓰기 후 문서 전체"다. 즉 description을
// 건드리지 않는 부분 업데이트(완료 처리, 마감일 변경, 칸반 재정렬 배치 등)에도
// 이 검증이 걸린다. 상한 도입 전에 2000자를 넘겨 저장된 문서가 있다면 그 문서는
// 모든 쓰기가 막혀 사용자가 복구할 방법이 없어진다 — writeBatch는 원자적이라
// 그런 문서 하나가 재정렬 배치 전체를 실패시킨다.
// 그래서 "새 값이 유효하거나, description을 바꾸지 않는 쓰기"를 허용한다.
function descriptionUnchanged() {
return request.resource.data.get("description", null)
== resource.data.get("description", null);
}
allow read: if request.auth != null && resource.data.userId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& descriptionValid();
allow update: if request.auth != null
&& resource.data.userId == request.auth.uid
&& request.resource.data.userId == resource.data.userId
&& (descriptionValid() || descriptionUnchanged());
allow delete: if request.auth != null && resource.data.userId == request.auth.uid;
}
match /feedback/{feedbackId} {
// 상한 1000은 클라이언트의 FEEDBACK_CONTENT_MAX_LENGTH
// (features/feedback/api/feedbackApi.ts)와 같은 값이어야 한다.
function contentValid() {
let c = request.resource.data.content;
return c is string && c.size() > 0 && c.size() <= 1000;
}
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.email == request.auth.token.email
&& contentValid()
&& request.resource.data.keys().hasOnly(["userId", "email", "content", "createdAt"])
&& request.resource.data.createdAt is string;
allow read, update, delete: if false;
}
match /calendarIntegrations/{userId} {
// premiumUntil(epoch 초) 커스텀 클레임이 지금보다 미래여야 한다. 만료 시각만 비교하므로
// 체험 종료·해지 후 기간 만료 시 별도 회수 작업이 필요 없다(billing-proxy가 설정).
allow read, write: if request.auth != null && request.auth.uid == userId
&& request.auth.token.premiumUntil is number
&& request.auth.token.premiumUntil > request.time.toMillis() / 1000;
}
match /userSettings/{userId} {
// 마감 알림 기본값. reminder-proxy는 서비스 계정(규칙 우회)으로 읽는다.
allow read, delete: if request.auth != null && request.auth.uid == userId;
allow create, update: if request.auth != null && request.auth.uid == userId
&& request.resource.data.keys().hasOnly(["reminderDefaultOffsetMinutes"])
&& (request.resource.data.reminderDefaultOffsetMinutes == "off"
|| request.resource.data.reminderDefaultOffsetMinutes in [0, 10, 30, 60, 1440]);
}
match /entitlements/{userId} {
// 구독 상태는 클라이언트가 절대 스스로 쓰지 못하게 막는다. 지금은 운영자가
// scripts/grantEntitlement.ts로 이 문서와 커스텀 클레임을 함께 설정하고,
// 결제 웹훅(billing-proxy)은 Admin SDK로 쓰는데 Admin SDK는 이 규칙을
// 우회하므로 write:false로 막아도 문제없다.
// write를 허용하면 브라우저 devtools에서 자기 자신의 plan을 premium으로
// 바꿔치기하는 게 가능해진다.
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if false;
}
}
}