1. 시작하기 전에
Cloud Firestore, Firebase용 Cloud Storage, 실시간 데이터베이스는 개발자가 작성한 구성 파일을 사용하여 읽기 및 쓰기 액세스 권한을 부여합니다. 보안 규칙이라고 하는 이 구성은 앱을 위한 일종의 스키마 역할을 할 수도 있습니다. 애플리케이션 개발에서 가장 중요한 부분 중 하나입니다. 이 Codelab에서 이에 관해 자세히 설명합니다.
기본 요건
- Visual Studio Code, Atom, Sublime Text와 같은 간단한 편집기
- Node.js 8.6.0 이상(Node.js를 설치하려면 nvm을 사용하고 버전을 확인하려면
node --version
를 실행하세요.) - Java 7 이상 (Java를 설치하려면 이 안내를 참고하고, 버전을 확인하려면
java -version
을 실행하세요.)
실습할 내용
이 Codelab에서는 Firestore를 기반으로 빌드된 간단한 블로그 플랫폼을 보호합니다. Firestore 에뮬레이터를 사용하여 보안 규칙에 대한 단위 테스트를 실행하고 규칙이 예상되는 액세스를 허용하거나 허용하지 않는지 확인합니다.
다음 작업을 수행하는 방법을 배우게 됩니다.
- 세분화된 권한 부여
- 데이터 및 유형 유효성 검사 시행
- 속성 기반 액세스 제어 구현
- 인증 방법에 따라 액세스 권한 부여
- 맞춤 함수 만들기
- 시간 기반 보안 규칙 만들기
- 거부 목록 및 소프트 삭제 구현
- 여러 액세스 패턴을 충족하기 위해 데이터를 비정규화해야 하는 경우 이해
2. 설정
블로그 애플리케이션입니다. 다음은 애플리케이션 기능에 대한 간략한 요약입니다.
블로그 게시물 초안:
- 사용자는
drafts
컬렉션에 있는 블로그 게시물 초안을 만들 수 있습니다. - 초안은 초안이 게시될 준비가 될 때까지 계속해서 업데이트할 수 있습니다.
- 게시할 준비가 되면
published
컬렉션에 새 문서를 만드는 Firebase 함수가 트리거됩니다. - 초안은 작성자 또는 사이트 운영자가 삭제할 수 있습니다.
게시된 블로그 게시물:
- 게시된 게시물은 사용자가 작성할 수 없으며 함수를 통해서만 만들 수 있습니다.
- 소프트 삭제만 가능하며
visible
속성을 false로 업데이트합니다.
설명
- 게시된 게시물에는 게시된 각 게시물의 하위 컬렉션인 댓글을 허용할 수 있습니다.
- 악용을 줄이기 위해 사용자가 댓글을 남기려면 확인된 이메일 주소가 있어야 하며 부정론자여서는 안 됩니다.
- 댓글은 게시 후 1시간 이내에만 업데이트할 수 있습니다.
- 댓글 작성자, 원본 게시물의 작성자 또는 운영자가 댓글을 삭제할 수 있습니다.
액세스 규칙 외에도 필수 필드와 데이터 검증을 시행하는 보안 규칙을 만듭니다.
모든 작업은 Firebase 에뮬레이터 도구 모음을 사용하여 로컬에서 이루어집니다.
소스 코드 가져오기
이 Codelab에서는 보안 규칙 테스트로 시작하지만 보안 규칙 자체는 최소한으로 테스트합니다. 따라서 먼저 테스트를 실행할 소스를 클론해야 합니다.
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
그런 다음 초기 상태 디렉터리로 이동하여 이 Codelab의 나머지 부분을 작업합니다.
$ cd codelab-rules/initial-state
이제 테스트를 실행할 수 있도록 종속 항목을 설치합니다. 인터넷 연결 속도가 느린 경우 1~2분 정도 걸릴 수 있습니다.
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Firebase CLI 가져오기
테스트를 실행하는 데 사용할 에뮬레이터 도구 모음은 Firebase CLI (명령줄 인터페이스)의 일부이며 다음 명령어를 사용하여 머신에 설치할 수 있습니다.
$ npm install -g firebase-tools
그런 다음 최신 버전의 CLI를 사용하고 있는지 확인합니다. 이 Codelab은 버전 8.4.0 이상에서 작동하지만 이후 버전에는 더 많은 버그 수정사항이 포함되어 있습니다.
$ firebase --version 9.10.2
3. 테스트 실행
이 섹션에서는 로컬에서 테스트를 실행합니다. 이제 에뮬레이터 모음을 부팅할 시간입니다.
에뮬레이터 시작
작업할 애플리케이션에는 세 가지 기본 Firestore 컬렉션이 있습니다. drafts
에는 진행 중인 블로그 게시물이 포함되고, published
컬렉션에는 게시된 블로그 게시물이 포함되어 있으며, comments
는 게시된 게시물의 하위 컬렉션입니다. 이 저장소에는 사용자가 drafts
, published
, comments
컬렉션에서 문서를 생성, 읽기, 업데이트, 삭제하는 데 필요한 사용자 속성 및 기타 조건을 정의하는 보안 규칙의 단위 테스트가 포함되어 있습니다. 이러한 테스트를 통과할 수 있도록 보안 규칙을 작성합니다.
먼저 데이터베이스가 잠깁니다. 데이터베이스에 대한 읽기 및 쓰기가 모두 거부되고 모든 테스트가 실패합니다. 보안 규칙을 작성하면 테스트가 통과됩니다. 테스트를 보려면 편집기에서 functions/test.js
를 엽니다.
명령줄에서 emulators:exec
를 사용하여 에뮬레이터를 시작하고 테스트를 실행합니다.
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
출력 상단으로 스크롤합니다.
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
현재 9개의 오류가 있습니다. 규칙 파일을 빌드할 때 더 많은 테스트가 통과하는 것을 확인하여 진행 상황을 측정할 수 있습니다.
4. 블로그 게시물 초안을 만듭니다.
초안 블로그 게시물에 대한 액세스 권한은 게시된 블로그 게시물에 대한 액세스와 매우 다르기 때문에 이 블로깅 앱은 초안 블로그 게시물을 별도의 컬렉션 /drafts
에 저장합니다. 초안은 작성자 또는 운영자만 액세스할 수 있으며 필수 필드와 변경 불가능한 필드에 대한 유효성 검사가 있습니다.
firestore.rules
파일을 열면 기본 규칙 파일이 표시됩니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
match 문 match /{document=**}
은 **
문법을 사용하여 하위 컬렉션의 모든 문서에 재귀적으로 적용합니다. 또한 최상위 수준이므로 현재로서는 누가 요청을 하거나 어떤 데이터를 읽거나 쓰려고 하는지에 관계없이 모든 요청에 동일한 포괄적 규칙이 적용됩니다.
먼저 가장 안쪽의 match 문을 삭제하고 match /drafts/{draftID}
로 바꿉니다. 문서 구조의 주석은 규칙에 유용할 수 있으며 이 Codelab에 포함될 예정입니다. 이는 항상 선택사항입니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
}
}
}
초안에 작성할 첫 번째 규칙은 문서를 만들 수 있는 사용자를 제어합니다. 이 신청서에서는 저자로 등록된 사용자만 초안을 만들 수 있습니다. 요청하는 사용자의 UID가 문서에 나열된 UID와 동일한지 확인합니다.
만들기의 첫 번째 조건은 다음과 같습니다.
request.resource.data.authorUID == request.auth.uid
다음으로, 필수 필드인 authorUID
, createdAt
, title
가 포함된 경우에만 문서를 만들 수 있습니다. 사용자는 createdAt
필드를 제공하지 않습니다. 따라서 앱에서 문서를 만들기 전에 추가해야 합니다. 속성이 생성되고 있는지만 확인하면 되므로 request.resource
에 이러한 모든 키가 있는지 확인할 수 있습니다.
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
블로그 게시물을 작성하기 위한 마지막 요구사항은 제목의 길이가 50자를 넘을 수 없다는 것입니다.
request.resource.data.title.size() < 50
이러한 모든 조건이 true여야 하므로 논리 AND 연산자 &&
를 사용하여 연결합니다. 첫 번째 규칙은 다음과 같이 변경됩니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
터미널에서 테스트를 다시 실행하고 첫 번째 테스트가 통과하는지 확인합니다.
5. 블로그 게시물 초안을 업데이트합니다.
다음으로, 작성자가 블로그 게시물 초안을 수정할 때 초안 문서를 수정합니다. 게시물을 업데이트할 수 있는 조건에 관한 규칙을 만듭니다. 먼저 작성자만 초안을 업데이트할 수 있습니다. 여기서는 이미 작성된 UID를 확인합니다.resource.data.authorUID
resource.data.authorUID == request.auth.uid
업데이트의 두 번째 요구사항은 authorUID
와 createdAt
라는 두 속성이 변경되면 안 된다는 것입니다.
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
마지막으로 제목은 50자(영문 기준) 이하여야 합니다.
request.resource.data.title.size() < 50;
이러한 조건을 모두 충족해야 하므로 &&
로 연결합니다.
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
전체 규칙은 다음과 같습니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
테스트를 다시 실행하고 다른 테스트가 통과하는지 확인합니다.
6. 삭제 및 초안 읽기: 속성 기반 액세스 제어
작성자는 초안을 만들고 업데이트할 수 있는 것처럼 초안을 삭제할 수도 있습니다.
resource.data.authorUID == request.auth.uid
또한 인증 토큰에 isModerator
속성이 있는 작성자는 초안을 삭제할 수 있습니다.
request.auth.token.isModerator == true
다음 조건 중 하나는 삭제에 충분하므로 논리 OR 연산자 ||
로 연결합니다.
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
읽기에도 동일한 조건이 적용되므로 규칙에 권한을 추가할 수 있습니다.
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
이제 전체 규칙은 다음과 같습니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
}
}
테스트를 다시 실행하고 이제 다른 테스트가 통과하는지 확인합니다.
7. 게시된 게시물 읽기, 생성, 삭제: 다양한 액세스 패턴에 대한 비정규화
게시된 게시물과 임시 게시물의 액세스 패턴이 매우 다르므로 이 앱은 게시물을 별도의 draft
및 published
컬렉션으로 비정규화합니다. 예를 들어 게시된 게시물은 누구나 읽을 수 있지만 완전히 삭제할 수는 없으며, 초안은 삭제할 수 있지만 저자와 운영자만 읽을 수 있습니다. 이 앱에서 사용자가 블로그 게시물 초안을 게시하려고 하면 새 게시물을 만드는 함수가 트리거됩니다.
다음으로 게시된 게시물의 규칙을 작성합니다. 가장 간단한 규칙은 게시된 게시물은 누구나 읽을 수 있지만 누구나 만들거나 삭제할 수 없다는 것입니다. 다음 규칙을 추가합니다.
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
이를 기존 규칙에 추가하면 전체 규칙 파일은 다음과 같이 됩니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
}
}
테스트를 다시 실행하고 다른 테스트를 통과하는지 확인합니다.
8. 게시된 게시물 업데이트: 맞춤 함수 및 로컬 변수
게시된 게시물을 업데이트하기 위한 조건은 다음과 같습니다.
- 작성자 또는 운영자만 할 수 있습니다.
- 모든 필수 입력란을 포함해야 합니다.
이미 작성자 또는 운영자 자격 요건을 작성했으므로 해당 자격 요건을 복사하여 붙여넣을 수 있지만 시간이 지남에 따라 읽고 유지하기 어려워질 수 있습니다. 대신 작성자 또는 운영자가 되기 위한 로직을 캡슐화하는 맞춤 함수를 만듭니다. 그런 다음 여러 조건에서 이를 호출합니다.
커스텀 함수 만들기
초안의 match 문 위에 post 문서 (초안 또는 게시된 글에서 작동함)와 사용자의 인증 객체를 인수로 사용하는 isAuthorOrModerator
라는 새 함수를 만듭니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
}
match /drafts/{postID} {
allow create: ...
allow update: ...
...
}
match /published/{postID} {
allow read: ...
allow create, delete: ...
}
}
}
로컬 변수 사용
함수 내에서 let
키워드를 사용하여 isAuthor
및 isModerator
변수를 설정합니다. 모든 함수는 return 문으로 끝나야 하며, 이 함수는 두 변수 중 하나가 true인지 나타내는 불리언을 반환합니다.
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
함수 호출
이제 resource.data
를 첫 번째 인수로 전달하도록 주의하면서 초안에서 이 함수를 호출하는 규칙을 업데이트합니다.
// Draft blog posts
match /drafts/{draftID} {
...
// Can be deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
이제 게시된 게시물을 업데이트하는 조건을 작성할 수 있으며 이 조건은 새 함수도 사용합니다.
allow update: if isAuthorOrModerator(resource.data, request.auth);
유효성 검사 추가
게시된 게시물의 일부 필드는 변경해서는 안 됩니다. 특히 url
, authorUID
, publishedAt
필드는 변경할 수 없습니다. 다른 두 필드 title
, content
, visible
는 업데이트 후에도 계속 있어야 합니다. 게시된 게시물의 업데이트에 다음 요구사항을 적용하려면 조건을 추가하세요.
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
])
맞춤 함수 직접 만들기
마지막으로 제목이 50자(영문 기준) 이하여야 한다는 조건을 추가합니다. 이는 재사용된 로직이므로 새 함수 titleIsUnder50Chars
를 만들어 이 작업을 실행할 수 있습니다. 새 함수를 사용하면 게시된 게시물을 업데이트하기 위한 조건은 다음과 같이 됩니다.
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
전체 규칙 파일은 다음과 같습니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
}
}
테스트를 다시 실행합니다. 이 시점에서 통과한 테스트 5개와 실패한 테스트 4개가 있어야 합니다.
9. 댓글: 하위 컬렉션 및 로그인 제공업체 권한
게시된 게시물에는 댓글이 허용되며 댓글은 게시된 게시물의 하위 컬렉션(/published/{postID}/comments/{commentID}
)에 저장됩니다. 기본적으로 컬렉션의 규칙은 하위 컬렉션에 적용되지 않습니다. 게시된 게시물의 상위 문서에 적용되는 규칙을 댓글에 적용하면 안 됩니다. 다른 규칙을 만들어야 합니다.
주석에 액세스하기 위한 규칙을 작성하려면 다음과 같이 match 문을 시작합니다.
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
댓글 읽기: 익명으로 할 수 없음
이 앱의 경우 익명 계정이 아닌 영구 계정을 만든 사용자만 댓글을 읽을 수 있습니다. 이 규칙을 적용하려면 각 auth.token
객체에 있는 sign_in_provider
속성을 조회합니다.
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
테스트를 다시 실행하고 테스트가 하나 더 통과하는지 확인합니다.
주석 만들기: 차단 목록 확인
댓글을 작성하려면 다음 세 가지 조건을 충족해야 합니다.
- 사용자에게 확인된 이메일이 있어야 합니다.
- 댓글은 500자 미만이어야 합니다.
bannedUsers
컬렉션의 Firestore에 저장된 차단된 사용자 목록에 포함될 수 없습니다. 다음 조건을 한 번에 하나씩 가져옵니다.
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
댓글을 작성하기 위한 마지막 규칙은 다음과 같습니다.
allow create: if
// User has verified email
(request.auth.token.email_verified == true) &&
// UID is not on bannedUsers list
!(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
이제 전체 규칙 파일은 다음과 같습니다.
For bottom of step 9
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
}
}
}
테스트를 다시 실행하고 테스트가 하나 더 통과하는지 확인합니다.
10. 댓글 업데이트: 시간 기반 규칙
댓글의 비즈니스 로직은 댓글 작성자가 작성 후 한 시간 동안 수정할 수 있다는 것입니다. 이를 구현하려면 createdAt
타임스탬프를 사용합니다.
먼저 사용자가 작성자임을 확인하려면 다음을 실행합니다.
request.auth.uid == resource.data.authorUID
다음으로, 최근 1시간 이내에 작성된 댓글입니다.
(request.time - resource.data.createdAt) < duration.value(1, 'h');
이 조건을 논리곱 연산자와 함께 사용하면 댓글 업데이트 규칙은 다음과 같이 됩니다.
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
테스트를 다시 실행하고 테스트가 하나 더 통과하는지 확인합니다.
11. 댓글 삭제: 상위 소유권 확인 중
댓글 작성자, 운영자 또는 블로그 게시물 작성자가 댓글을 삭제할 수 있습니다.
먼저 앞서 추가한 도우미 함수는 게시물이나 댓글에 존재할 수 있는 authorUID
필드를 확인하므로 도우미 함수를 재사용하여 사용자가 작성자인지 운영자인지 확인할 수 있습니다.
isAuthorOrModerator(resource.data, request.auth)
사용자가 블로그 게시물 작성자인지 확인하려면 get
를 사용하여 Firestore에서 게시물을 조회합니다.
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
이러한 조건 중 하나라도 충분하므로 조건 사이에 논리 OR 연산자를 사용합니다.
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
테스트를 다시 실행하고 테스트가 하나 더 통과하는지 확인합니다.
전체 규칙 파일은 다음과 같습니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
}
}
}
12. 다음 단계
수고하셨습니다. 모든 테스트를 통과하고 애플리케이션을 보호하는 보안 규칙을 작성했습니다.
다음은 관련 주제입니다.