Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion firestore-tests/rules.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,22 @@ describe("publicProfiles", () => {
await assertFails(setDoc(doc(db, "publicProfiles", OTHER), { id: OTHER, name: "가로채기" }));
});

it("계정을 삭제할 때 본인 공개 프로필도 지운다", async () => {
// UserRepository.deleteUserDocument 와 같은 순서다. 사용자 문서가 없어도 본인 확인은
// 인증 uid 로 해야 하며, 삭제에는 request.resource 의 새 필드가 없다.
const db = owner();
await assertSucceeds(deleteDoc(doc(db, "shareCodes", SHARE_CODE)));
await assertSucceeds(deleteDoc(doc(db, "users", OWNER)));
await assertSucceeds(deleteDoc(doc(db, "publicProfiles", OWNER)));
assert.equal((await getDoc(doc(db, "publicProfiles", OWNER))).exists(), false);
await assertSucceeds(deleteDoc(doc(db, "publicProfiles", OWNER)));
});

it("남의 공개 프로필이나 로그인하지 않은 요청으로는 지우지 못한다", async () => {
await assertFails(deleteDoc(doc(other(), "publicProfiles", OWNER)));
await assertFails(deleteDoc(doc(anonymous(), "publicProfiles", OWNER)));
});

it("이름 밖의 값은 넣지 못한다", async () => {
// 이메일이나 토큰이 공개 프로필로 새어 나가지 않게 필드를 묶는다.
const db = owner();
Expand Down Expand Up @@ -242,19 +258,54 @@ describe("schedules", () => {
await assertSucceeds(
setDoc(doc(db, "schedules", "new-1"), { id: "new-1", userId: OWNER, title: "새 일정", sharedCode: "" }),
);
await assertSucceeds(updateDoc(doc(db, "schedules", "own-1"), { title: "바꾼 일정" }));
await assertSucceeds(deleteDoc(doc(db, "schedules", "own-1")));
});

it("남의 비공개 일정은 읽지 못한다", async () => {
await assertFails(getDoc(doc(other(), "schedules", "own-1")));
});

it("남의 일정을 자기 것으로 만들지 못한다", async () => {
it("남의 소유자 ID로 일정을 만들지 못한다", async () => {
await assertFails(
setDoc(doc(other(), "schedules", "steal-1"), { id: "steal-1", userId: OWNER, title: "가로채기" }),
);
});

for (const scheduleId of ["own-1", "shared-1"]) {
it(`${scheduleId}의 기존 소유자를 타인이 바꾸지 못한다`, async () => {
// 일부 필드 수정과 문서 교체를 모두 확인한다. 공유 코드를 받은 가족도 소유자는 아니다.
await assertFails(updateDoc(doc(other(), "schedules", scheduleId), { userId: OTHER }));
await assertFails(updateDoc(doc(stranger(), "schedules", scheduleId), { userId: STRANGER }));
await assertFails(
setDoc(doc(other(), "schedules", scheduleId), {
id: scheduleId, userId: OTHER, title: "바뀐 일정", sharedCode: "",
}),
);
await assertFails(
setDoc(doc(stranger(), "schedules", scheduleId), {
id: scheduleId, userId: STRANGER, title: "바뀐 일정", sharedCode: "",
}),
);
});
}

it("본인 일정도 다른 사람에게 소유권을 넘기지 못한다", async () => {
await assertFails(updateDoc(doc(owner(), "schedules", "own-1"), { userId: OTHER }));
});

it("남의 일정은 지우지 못한다", async () => {
await assertFails(deleteDoc(doc(other(), "schedules", "own-1")));
await assertFails(deleteDoc(doc(other(), "schedules", "shared-1")));
});

it("로그인하지 않으면 일정을 만들거나 고치거나 지우지 못한다", async () => {
const db = anonymous();
await assertFails(setDoc(doc(db, "schedules", "new-1"), { userId: OWNER }));
await assertFails(updateDoc(doc(db, "schedules", "own-1"), { title: "바뀐 일정" }));
await assertFails(deleteDoc(doc(db, "schedules", "own-1")));
});

it("코드를 등록한 가족은 공유 일정을 조회하고 완료만 바꾼다", async () => {
const db = other();
await assertSucceeds(getDocs(query(collection(db, "schedules"), where("sharedCode", "==", SHARE_CODE))));
Expand Down
13 changes: 8 additions & 5 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ service cloud.firestore {
match /publicProfiles/{userId} {
allow get: if isSignedIn();
allow list: if false;
allow write: if isSelf(userId)
allow create, update: if isSelf(userId)
&& request.resource.data.keys().hasOnly(['id', 'name'])
&& request.resource.data.id == userId;
allow delete: if isSelf(userId);
}

// 공유 코드 등록부. 코드 자체가 문서 이름이다.
Expand All @@ -56,10 +57,12 @@ service cloud.firestore {

// 일정
match /schedules/{scheduleId} {
// 본인이 만든 일정
allow read, write: if isSignedIn() &&
(request.auth.uid == resource.data.userId ||
request.auth.uid == request.resource.data.userId);
// 생성할 때 소유자를 확인하고, 기존 일정은 저장된 소유자를 기준으로 확인한다.
// 일정을 고칠 때도 소유자는 그대로 유지한다.
allow read, delete: if isSelf(resource.data.userId);
allow create: if isSelf(request.resource.data.userId);
allow update: if isSelf(resource.data.userId)
&& request.resource.data.userId == resource.data.userId;

// 공유 일정은 그 코드의 감시자로 등록한 사람만 읽는다.
allow read: if resource.data.sharedCode is string &&
Expand Down