Firestore permission-denied 오류에서 Rules와 쿼리 조건은 어떻게 확인하나요?
Firestore permission-denied는 요청 작업·경로·소유자 규칙·쿼리를 함께 봅니다. Rules는 남의 문서를 걸러 주는 필터가 아닙니다. 본인·타인·비로그인 요청을 비교하며 권한을 모두 여는 방식으로 해결하지 않습니다.
적용 조건·범위: Missing or insufficient permissions.와 code: permission-denied가 난 작업을 get, list, create, update, delete 중 하나로 기록합니다. 개발 환경에 ownerId가 alice인 notes/a와 bob인 notes/b를 준비합니다. 아래 규칙에서 alice로 전체 notes 목록을 읽으면 소유자 범위를 보장하지 못해 거부될 수 있습니다. Admin SDK 테스트는 Rules를 우회하므로 이 재현에 사용하지 않습니다.
본문 근거: firebase.google.com/docs/firestore/security/rules-query · firebase.google.com/docs/firestore/security/rules-conditions · firebase.google.com/docs/firestore/security/test-rules-emulator
학습 목표와 사전 지식
목표: permission-denied를 재현한 요청과 소유자 규칙·쿼리를 짝지어 수정합니다.
사전 지식: Firestore 컬렉션/문서 경로, Auth UID, 개발용 Emulator
Firestore Rules는 결과에서 비허용 문서만 걸러주지 않습니다. 요청이 허용되는 범위만 읽는다는 것을 쿼리가 증명해야 합니다. 로그인 성공만으로 타인의 문서를 읽을 권한이 생기지는 않습니다.
증상과 최소 재현
Missing or insufficient permissions.와 code: permission-denied가 난 작업을 get, list, create, update, delete 중 하나로 기록합니다. 개발 환경에 ownerId가 alice인 notes/a와 bob인 notes/b를 준비합니다. 아래 규칙에서 alice로 전체 notes 목록을 읽으면 소유자 범위를 보장하지 못해 거부될 수 있습니다. Admin SDK 테스트는 Rules를 우회하므로 이 재현에 사용하지 않습니다.
firestore.rules · 전체 파일; 소유자 메모 최소 규칙
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /notes/{noteId} {
function signedIn() { return request.auth != null; }
function owner() {
return signedIn() && resource.data.ownerId == request.auth.uid;
}
allow read, delete: if owner();
allow create: if signedIn()
&& request.resource.data.keys().hasOnly(['ownerId', 'text'])
&& request.resource.data.ownerId == request.auth.uid
&& request.resource.data.text is string
&& request.resource.data.text.size() <= 500;
allow update: if owner()
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['text'])
&& request.resource.data.text is string
&& request.resource.data.text.size() <= 500;
}
}
}
src/notes.js · 전체 모듈; db와 인증 관찰이 끝난 user를 전달
import { collection, getDocs, query, where } from 'firebase/firestore';
export async function readMyNotes(db, user) {
if (!user) throw new Error('로그인 후 실행하세요.');
const q = query(collection(db, 'notes'), where('ownerId', '==', user.uid));
const snapshot = await getDocs(q);
return snapshot.docs.map((item) => ({ id: item.id, ...item.data() }));
}

수정 위치와 이유
src/notes.js의 getDocs(collection(db, "notes")) 호출을 위 소유자 쿼리로 교체합니다. Rules를 모두 열어 성공시키는 방식은 사용하지 않습니다. create는 새 문서(request.resource.data)를 검사하고 update는 기존 소유자(resource.data)를 확인한 뒤 변경 가능한 필드를 text로 제한합니다. ownerId를 바꾸는 업데이트는 거부됩니다. delete에는 새 문서가 없으므로 기존 문서로 소유권을 판단합니다.
| 시도 | 기대 결과 |
|---|---|
| alice가 ownerId: alice 메모 작성 | 허용 |
| alice가 ownerId: bob 메모 작성 | 거부 |
| alice가 notes/b 단건 읽기 | 거부 |
| alice가 ownerId == alice 목록 조회 | 허용 |
| alice가 자신의 ownerId 변경 | 거부 |
| 비로그인 목록 조회 | 거부 |
수정 후에도 실패하면
onAuthStateChanged의 최초 결과 전에 요청하지 않았는지, 앱 projectId와 배포 대상, 에뮬레이터 연결 여부, 정확한 match 경로를 확인합니다. 배포된 규칙 전파에 시간이 걸릴 수 있습니다. 실패가 failed-precondition으로 바뀌면 필요한 인덱스 문제를 따로 확인하세요. 규칙 합치기 전 다른 경로의 넓은 allow가 OR로 접근을 허용하지 않는지도 검토합니다.
일곱 요청을 Emulator에서 검증하기
아래는 본문 firestore.rules를 저장한 폴더에서 실행하는 최소 부정 테스트입니다. firebase.json과 scripts/rules-check.mjs 파일을 각각 만들고 Firebase CLI·Java 런타임을 준비하세요. 앱의 실제 프로젝트와 분리된 demo-permission 프로젝트 ID를 사용합니다.
firebase.json
{
"firestore": { "rules": "firestore.rules" },
"emulators": { "firestore": { "port": 8080 } }
}
scripts/rules-check.mjs
import { readFileSync } from 'node:fs';
import { assertFails, assertSucceeds, initializeTestEnvironment } from '@firebase/rules-unit-testing';
import { collection, doc, getDoc, getDocs, query, setDoc, updateDoc, where } from 'firebase/firestore';
const env = await initializeTestEnvironment({
projectId: 'demo-permission',
firestore: { rules: readFileSync('firestore.rules', 'utf8') },
});
try {
await env.clearFirestore();
await env.withSecurityRulesDisabled(async (context) => {
await setDoc(doc(context.firestore(), 'notes/a'), { ownerId: 'alice', text: 'A' });
await setDoc(doc(context.firestore(), 'notes/b'), { ownerId: 'bob', text: 'B' });
});
const alice = env.authenticatedContext('alice').firestore();
const guest = env.unauthenticatedContext().firestore();
await assertFails(getDocs(collection(alice, 'notes')));
const mine = await assertSucceeds(getDocs(query(
collection(alice, 'notes'), where('ownerId', '==', 'alice')
)));
if (mine.size !== 1 || mine.docs[0].id !== 'a') throw new Error('내 목록 결과 확인 실패');
await assertFails(getDoc(doc(alice, 'notes/b')));
await assertFails(getDocs(query(collection(guest, 'notes'), where('ownerId', '==', 'alice'))));
await assertFails(setDoc(doc(alice, 'notes/c'), { ownerId: 'bob', text: 'X' }));
await assertSucceeds(setDoc(doc(alice, 'notes/c'), { ownerId: 'alice', text: 'C' }));
await assertFails(updateDoc(doc(alice, 'notes/c'), { ownerId: 'bob' }));
console.log('목록·단건·생성·소유권 변경 검사 통과');
} finally {
await env.cleanup();
}
npm init -y
npm install firebase @firebase/rules-unit-testing
firebase emulators:exec --project demo-permission --only firestore "node scripts/rules-check.mjs"
첫 요청은 소유자 조건 없는 전체 목록이므로 거부, 둘째는 alice 문서 한 개를 반환해야 합니다. 단건의 bob 문서·비로그인 목록·bob 소유자로 생성·ownerId 변조는 거부되고 alice 소유자 생성은 허용돼야 합니다. 실패하면 어느 assert에서 어긋났는지와 인증 UID·경로·작업을 기록하세요. 이 검사는 Rules Emulator가 실제로 실행돼야만 결과를 알 수 있으며 이 편집에서는 실행하지 않았습니다.
직접 확인할 결과
위 코드의 7개 요청을 클라이언트 인증 문맥별로 실행
규칙 배포 프로젝트 및 파일 비교
실패 코드와 요청 종류를 기록
검증 범위와 기준일
공식 문서 확인일: 2026-09-13. 웹 SDK 예제는 모듈형 API 기준입니다. 이 글의 코드·구조 정적 검토와 실제 클라우드 인증·저장·배포 검증을 구분합니다. 본인 개발 Firebase 프로젝트의 실제 인증·권한·배포 요청은 이 편집 환경에서 실행하지 않았습니다.
이어서 학습하기
- Firebase Auth unauthorized-domain 오류 해결: 로그인 도메인 설정 확인
- Firebase Storage 이미지 오류 해결: 403·404·token·Rules 확인법
공식 문서
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 20개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.