Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기

사용자 및 그룹을 위한 안전한 데이터 액세스

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

많은 협업 앱에서 사용자는 권한 집합에 따라 다양한 데이터를 읽고 쓸 수 있습니다. 예를 들어 문서 편집 앱에서 사용자는 일부 사용자가 문서를 읽고 쓸 수 있도록 허용하면서 원치 않는 액세스를 차단할 수 있습니다.

솔루션: 역할 기반 액세스 제어

Cloud Firestore의 데이터 모델과 사용자 지정 보안 규칙 을 활용하여 앱에서 역할 기반 액세스 제어를 구현할 수 있습니다.

사용자가 다음 보안 요구 사항으로 "스토리" 및 "댓글"을 작성할 수 있는 협업 작성 애플리케이션을 구축한다고 가정합니다.

  • 각 스토리에는 한 명의 소유자가 있으며 "작가", "댓글 작성자" 및 "독자"와 공유할 수 있습니다.
  • 독자 는 스토리와 댓글만 볼 수 있습니다. 그들은 아무 것도 편집할 수 없습니다.
  • 댓글 작성자 는 독자의 모든 액세스 권한을 가지며 스토리에 댓글을 추가할 수도 있습니다.
  • 작성자 는 댓글 작성자의 모든 액세스 권한이 있으며 스토리 콘텐츠를 편집할 수도 있습니다.
  • 소유자 는 스토리의 모든 부분을 편집하고 다른 사용자의 액세스를 제어할 수 있습니다.

데이터 구조

앱에 각 문서가 스토리를 나타내는 stories 컬렉션이 있다고 가정합니다. 각 스토리에는 각 문서가 해당 스토리에 대한 댓글인 comments 하위 컬렉션도 있습니다.

액세스 역할을 추적하려면 역할에 대한 사용자 ID의 맵인 roles 필드를 추가하십시오.

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}

댓글에는 작성자의 사용자 ID와 일부 콘텐츠라는 두 개의 필드만 포함됩니다.

/stories/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

규칙

이제 데이터베이스에 사용자의 역할이 기록되었으므로 이를 검증하기 위한 보안 규칙을 작성해야 합니다. 이 규칙은 앱이 Firebase 인증 을 사용하므로 request.auth.uid 변수가 사용자의 ID라고 가정합니다.

1단계 : 스토리 및 댓글에 대한 빈 규칙이 포함된 기본 규칙 파일로 시작합니다.

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
         // TODO: Story rules go here...

         match /comments/{comment} {
            // TODO: Comment rules go here...
         }
     }
   }
}

2단계 : 소유자에게 스토리를 완전히 제어할 수 있는 간단한 write 규칙을 추가합니다. 정의된 기능은 사용자의 역할을 결정하고 새 문서가 유효한지 여부를 결정하는 데 도움이 됩니다.

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          // Read from the "roles" map in the resource (rsc).
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          // Determine if the user is one of an array of roles
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          // Valid if story does not exist and the new story has the correct owner.
          return resource == null && isOneOfRoles(request.resource, ['owner']);
        }

        // Owners can read, write, and delete stories
        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

         match /comments/{comment} {
            // ...
         }
     }
   }
}

3단계 : 모든 역할의 사용자가 스토리와 댓글을 읽을 수 있도록 허용하는 규칙을 작성합니다. 이전 단계에서 정의한 함수를 사용하면 규칙을 간결하고 읽기 쉽게 유지할 수 있습니다.

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

        // Any role can read stories.
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          // Any role can read comments.
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
        }
     }
   }
}

4단계 : 스토리 작성자, 댓글 작성자 및 소유자가 댓글을 게시할 수 있도록 허용합니다. 이 규칙은 또한 주석의 owner 가 요청한 사용자와 일치하는지 확인하여 사용자가 서로의 주석을 덮어쓰는 것을 방지합니다.

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner'])
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);

          // Owners, writers, and commenters can create comments. The
          // user id in the comment document must match the requesting
          // user's id.
          //
          // Note: we have to use get() here to retrieve the story
          // document so that we can check the user's role.
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

5단계 : 작성자에게 스토리 콘텐츠를 편집할 수 있는 권한을 부여하지만 스토리 역할을 편집하거나 문서의 다른 속성을 변경할 수는 없습니다. 작성자는 스토리만 업데이트할 수 있으므로 스토리 write 규칙을 create , updatedelete 에 대한 별도의 규칙으로 분할해야 합니다.

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return request.resource.data.roles[request.auth.uid] == 'owner';
        }

        function onlyContentChanged() {
          // Ensure that title and roles are unchanged and that no new
          // fields are added to the document.
          return request.resource.data.title == resource.data.title
            && request.resource.data.roles == resource.data.roles
            && request.resource.data.keys() == resource.data.keys();
        }

        // Split writing into creation, deletion, and updating. Only an
        // owner can create or delete a story but a writer can update
        // story content.
        allow create: if isValidNewStory();
        allow delete: if isOneOfRoles(resource, ['owner']);
        allow update: if isOneOfRoles(resource, ['owner'])
                      || (isOneOfRoles(resource, ['writer']) && onlyContentChanged());
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

제한 사항

위에 표시된 솔루션은 보안 규칙을 사용하여 사용자 데이터를 보호하는 방법을 보여주지만 다음 제한 사항에 유의해야 합니다.

  • 세분성 : 위의 예에서 여러 역할(작성자 및 소유자)은 동일한 문서에 대한 쓰기 액세스 권한을 갖지만 제한 사항은 다릅니다. 이것은 더 복잡한 문서로 관리하기 어려울 수 있으며 단일 문서를 각각 단일 역할이 소유한 여러 문서로 분할하는 것이 더 나을 수 있습니다.
  • 대규모 그룹 : 매우 크거나 복잡한 그룹과 공유해야 하는 경우 역할이 대상 문서의 필드가 아닌 자체 컬렉션에 저장되는 시스템을 고려하십시오.