안전하게 데이터 쿼리

이 페이지에서는 보안 규칙 구조화보안 규칙 조건 작성의 개념을 바탕으로 Cloud Firestore 보안 규칙에서 쿼리를 활용하는 방법을 보여줍니다. 작성할 수 있는 쿼리에 보안 규칙이 어떤 영향을 미치는지 자세히 살펴보고 쿼리에서 보안 규칙과 동일한 제약조건을 사용하게 하는 방법을 설명합니다. 또한 limitorderBy와 같은 쿼리 속성을 기준으로 쿼리를 허용 또는 거부하도록 보안 규칙을 작성하는 방법도 설명합니다.

규칙과 필터의 차이

쿼리를 작성하여 문서를 가져올 때 보안 규칙은 필터가 아니므로 전부 쿼리하거나 전혀 쿼리하지 않는다는 점에 유의해야 합니다. 시간과 리소스를 절약하기 위해 Cloud Firestore에서 모든 문서의 실제 필드 값 대신 잠재적인 결과 세트로 쿼리를 평가합니다. 쿼리가 클라이언트에 읽기 권한이 없는 문서를 반환할 수도 있는 경우에 전체 요청이 실패합니다.

쿼리 및 보안 규칙

아래 예시에서 볼 수 있듯이 보안 규칙의 제약조건에 맞게 쿼리를 작성해야 합니다.

auth.uid를 기반으로 문서 보호 및 쿼리

다음 예시는 쿼리를 작성하여 보안 규칙으로 보호하는 문서를 가져오는 방법을 보여줍니다. story 문서의 컬렉션이 포함된 데이터베이스를 예로 들어보겠습니다.

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

titlecontent 필드 외에도 각 문서가 액세스 제어에 사용할 authorpublished 필드를 저장합니다. 이 예시에서는 앱이 Firebase 인증을 사용하여 author 필드를 문서 생성자 UID로 설정한다고 가정합니다. 또한 Firebase 인증이 보안 규칙의 request.auth 변수를 채웁니다.

다음 보안 규칙은 request.authresource.data 변수를 사용하여 각 story의 읽기 및 쓰기 액세스를 해당 작성자로 제한합니다.

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

앱에 자신이 작성한 story 문서의 목록을 사용자에게 표시하는 페이지가 있다고 가정해 보겠습니다. 다음 쿼리를 사용하여 이 페이지를 채울 수 있을 것이라 생각할 수도 있지만 이 쿼리에 보안 규칙과 동일한 제약조건이 없으므로 쿼리가 실패합니다.

무효: 쿼리 제약조건이 보안 규칙 제약조건과 일치하지 않습니다.

// This query will fail
db.collection("stories").get()

현재 사용자가 실제로 모든 story 문서의 작성자인 경우에도 쿼리가 실패합니다. 이러한 동작이 발생하는 이유는 Cloud Firestore가 보안 규칙을 적용할 때 데이터베이스에 있는 문서의 실제 속성이 아니라 잠재적인 결과 세트로 쿼리를 평가하기 때문입니다. 쿼리에 보안 규칙을 위반하는 문서가 포함될 수도 있는 경우에 쿼리가 실패합니다.

반면에 다음 쿼리는 성공하는데, 그 이유는 보안 규칙과 동일한 제약조건을 author 필드에 포함하기 때문입니다.

유효: 쿼리 제약조건이 보안 규칙 제약조건과 일치합니다.

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

필드를 기준으로 문서 보안 적용 및 쿼리

쿼리와 규칙의 상호작용을 더 자세히 보여주기 위해 아래 보안 규칙은 모든 사용자가 published 필드가 true로 설정된 story 문서를 읽을 수 있도록 stories 컬렉션의 읽기 액세스 권한을 확장합니다.

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

게시된 페이지의 쿼리에는 보안 규칙과 동일한 제약조건을 포함해야 합니다.

db.collection("stories").where("published", "==", true).get()

쿼리 제약조건 .where("published", "==", true)resource.data.published가 모든 결과에 대해 true라고 보장합니다. 따라서 이 쿼리는 보안 규칙을 충족하며 데이터를 읽을 수 있습니다.

OR 쿼리

규칙 세트에 대해 논리적 OR 쿼리 (or, in 또는 array-contains-any)를 평가할 때 Cloud Firestore는 각 비교 값을 별도로 평가합니다. 각 비교 값은 보안 규칙 제약조건을 충족해야 합니다. 다음 규칙의 경우를 예로 들어보겠습니다.

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

무효: 쿼리가 모든 잠재적 문서에 대해 x > 5의 조건을 보장하지 않습니다.

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

유효: 쿼리가 모든 잠재적 문서에 대해 x > 5의 조건을 보장합니다.

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [6, 42, 99, 105, 200])
    )

쿼리의 제약조건 평가

또한 보안 규칙은 제약조건을 기준으로 쿼리를 허용하거나 거부할 수 있습니다. request.query 변수에는 쿼리의 limit, offset, orderBy 속성이 포함됩니다. 예를 들어 보안 규칙은 가져오는 최대 문서 수를 특정 범위로 제한하지 않는 쿼리를 거부할 수 있습니다.

allow list: if request.query.limit <= 10;

다음 규칙 세트는 쿼리에 적용된 제약조건을 평가하는 보안 규칙을 작성하는 방법을 보여줍니다. 이 예시는 다음 변경사항을 포함하여 이전 stories 규칙 세트를 확장합니다.

  • 규칙 세트가 읽기 규칙을 getlist 규칙으로 분리합니다.
  • get 규칙은 문서 검색을 공개 문서나 사용자가 작성한 문서로 제한합니다.
  • list 규칙은 get과 동일한 제한사항을 쿼리에 적용합니다. 또한 쿼리 한도를 확인한 다음 한도가 없거나 10을 초과하는 쿼리를 거부합니다.
  • 규칙 세트에서 코드 중복을 피하기 위해 authorOrPublished() 함수를 정의합니다.
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

컬렉션 그룹 쿼리 및 보안 규칙

기본적으로 쿼리는 단일 컬렉션으로 범위가 지정되며 해당 컬렉션에서만 결과를 검색합니다. 컬렉션 그룹 쿼리를 사용하면 ID가 동일한 모든 컬렉션으로 구성된 컬렉션 그룹에서 결과를 검색할 수 있습니다. 이 섹션에서는 보안 규칙을 사용하여 컬렉션 그룹 쿼리를 보호하는 방법을 설명합니다.

컬렉션 그룹을 기반으로 문서 보호 및 쿼리

보안 규칙에서 컬렉션 그룹에 대한 규칙을 작성하여 컬렉션 그룹 쿼리를 명시적으로 허용해야 합니다.

  1. 규칙 세트의 첫 번째 줄에 rules_version = '2';가 있는지 확인합니다. 컬렉션 그룹 쿼리에는 보안 규칙 버전 2의 새로운 재귀 와일드 카드 {name=**} 동작이 필요합니다.
  2. match /{path=**}/[COLLECTION_ID]/{doc}를 사용하여 컬렉션 그룹의 규칙을 작성합니다.

예를 들어 posts 하위 컬렉션을 포함하는 forum 문서로 구성된 포럼을 살펴보겠습니다.

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

이 애플리케이션에서는 소유자가 글을 편집하고 인증된 사용자가 글을 읽을 수 있도록 합니다.

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

모든 인증된 사용자는 단일 포럼의 글을 검색할 수 있습니다.

db.collection("forums/technology/posts").get()

그런데 현재 사용자에게 모든 포럼에 있는 자신의 글을 표시하려면 어떻게 해야 할까요? 컬렉션 그룹 쿼리를 사용하여 모든 posts 컬렉션에서 결과를 검색할 수 있습니다.

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

보안 규칙에서 posts 컬렉션 그룹에 대한 read 또는 list 규칙을 작성하여 이 쿼리를 허용해야 합니다.

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

그러나 이러한 규칙은 계층 구조와 상관없이 ID가 posts인 모든 컬렉션에 적용됩니다. 예를 들어 이러한 규칙은 다음과 같은 posts 컬렉션 모두에 적용됩니다.

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

필드를 기반으로 컬렉션 그룹 쿼리 보호

단일 컬렉션 쿼리와 마찬가지로 컬렉션 그룹 쿼리도 보안 규칙에 의해 설정된 제약조건을 충족해야 합니다. 예를 들어 위의 stories 예시에서 했던 것처럼 published 필드를 각 포럼 글에 추가할 수 있습니다.

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

그런 다음 published 상태와 글 author를 기반으로 posts 컬렉션 그룹에 대한 규칙을 작성할 수 있습니다.

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

이러한 규칙을 사용하면 웹, Apple, Android 클라이언트에서 다음 쿼리를 수행할 수 있습니다.

  • 누구나 포럼에서 게시된 글을 검색할 수 있습니다.

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • 누구나 모든 포럼에서 저자의 게시된 글을 검색할 수 있습니다.

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • 저자는 모든 포럼에서 게시된 글과 게시되지 않은 글을 모두 검색할 수 있습니다.

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

컬렉션 그룹 및 문서 경로를 기반으로 문서 보호 및 쿼리

경우에 따라 문서 경로를 기반으로 컬렉션 그룹 쿼리를 제한할 수 있습니다. 이러한 제한사항을 만들려면 필드를 기반으로 문서를 보호하고 쿼리할 때 동일한 기법을 사용하면 됩니다.

여러 주식 및 암호화폐 교환 중에 각 사용자의 트랜잭션을 추적하는 애플리케이션을 살펴보겠습니다.

/users/{userid}/exchange/{exchangeid}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

user 필드를 확인합니다. 문서의 경로를 보면 어떤 사용자가 transaction 문서를 소유하는지 알 수 있지만 다음과 같은 두 가지 작업을 수행할 수 있도록 각 transaction 문서에 이 정보가 복제됩니다.

  • 문서 경로에 특정 /users/{userid}가 포함된 문서로 제한된 컬렉션 그룹 쿼리를 작성합니다. 예를 들면 다음과 같습니다.

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • 한 사용자가 다른 사용자의 transaction 문서를 검색할 수 없도록 transactions 컬렉션 그룹의 모든 쿼리에 이 제한사항을 적용합니다.

이러한 제한사항을 보안 규칙에 적용하고 user 필드에 대한 데이터 유효성 검사를 포함합니다.

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

다음 단계