diff --git a/firestore-tests/rules.test.mjs b/firestore-tests/rules.test.mjs index 564df7b..3dfed65 100644 --- a/firestore-tests/rules.test.mjs +++ b/firestore-tests/rules.test.mjs @@ -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(); @@ -242,6 +258,7 @@ 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"))); }); @@ -249,12 +266,46 @@ describe("schedules", () => { 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)))); diff --git a/firestore.rules b/firestore.rules index 168ab5b..7ed1cdc 100644 --- a/firestore.rules +++ b/firestore.rules @@ -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); } // 공유 코드 등록부. 코드 자체가 문서 이름이다. @@ -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 &&