ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Firebase] 파이어베이스 보안규칙 (Firestore Security Rules ) 작성 방법
    데이터베이스/Firebase 2020. 1. 8. 12:21
    반응형

     

     

    파이어베이스에서 제공하는 보안규칙은 코드가 간단하며

    보안규칙을 위해 인프라를 관리하거나 복잡한 서버측 인증 및 인증 코드를 작성할 필요 없다.

     

    하지만 보안규칙을 적용하지 않으면 파이어베이스는 디폴트로 데이터베이스를 보호해주지 않기 때문에

    보안규칙 적용이 꼭 필요하다. 실제로 개발자가 앱을 제작할 때 코드 상에서만 규칙을 적용하고 이를 간과해 데이터가 노출된 사례들이 있다.

     

    그래서 나는 보안규칙 작성에 대한 규칙들을 정리해보았다.

     

     


     

    1. 보안규칙 버전 작성

     

    rules_version = '2';

     

    기존의 rules_version = '1' 은 와일드 카드를 사용할 때 한 개 혹은 두 개 이상의 path를 포함하고 있어야하기 때문에 자유롭게 와일드카드를 사용하기 위해서는 zero path를 허용하는 버전 2를 사용하도록 하자

     

     

    2. 서비스 및 데이터 베이스 선언

     

    service cloud.firestore {
    match /databases/{database}/documents

     

    • service cloud.firestore { : 규칙의 범위를 cloud.firestore로 지정해 cloud firestore 보안규칙과 다른 제품의 규칙간의 충돌 방지한다.

    • 구체화시킬 path의 pattern을 매치한다. 프로젝트의 모든 cloud firestore 데이터 베이스가 일치하도록 지정한다.

     

    3. 보안규칙 작성

     

    기본구조

    service cloud.firestore {
    
      match /databases/{database}/documents {
          match /stories/{storyid} {
          allow write: if request.auth.uid == resource.data.author;
            }
        }
    } 

     

     

    1) 규칙을 적용할 문서를 지정한다.

     

     

    - cities collection 내 seoul이라는 특정 문서에 대해 보안규칙 적용

    match /cities/ Seoul {
    

     

    - cities collection 내의 모든 문서에 대해 보안규칙 적용 (하위 컬렉션은 적용되지 않는다.)

    match /cities/{city} {

     

    - cities collection 내 모든 하위 범주에 대해 보안규칙 적용

    match /cities/{document=**}

     

    * { } 내의 이름은 컬렉션 이름과 다르게 임의로 설정하면 된다.

     

     

    2) 지정 문서에 대한 보안 규칙을 정의한다.

    a. 행동(action) 정의

     

    • 읽기
    allow read: if <condition>;
    
    allow get: if <condition>;
    allow list: if <condition>;

     

    read : 문서를 읽거나 가져오는 행동 ( get + list )

    get : 단일문서를 읽거나 가져오는 행동

    Firestore.instance
                 .collection(‘users’)
                    .document(uid)
                    .get()
    

    list : 쿼리 문서 혹은 collection내 모든 문서를 읽거나 가져오는 행동

    Firestore.instance
            .collection(‘users’)
            .get()
    

     

    • 쓰기
    allow write: if <condition>;
    
    allow create: if <condition>;
    allow update: if <condition>;
    allow delete: if <condition>;

    write : 문서를 쓰는 행동 ( create + update + delete )

     

    create : 문서를 생성하는 행동

    update : 문서를 수정하는 행동

    delete : 문서를 삭제하는 행동

     

     

    b. 보안 조건 정의

     

     

    데이터에 접근할 사용자에 대한 보안조건


    - 모든사람들이 읽고 쓰기 가능

    allow read, write: if true

     

    - 콘텐츠 소유자만 읽고 쓰기 가능

    allow read, write: if request.auth.uid == request.resource.data.author_uid

     

    - 모든 접근을 차단

    allow read, write: if false
    

     

    - 그 외 사용자 인증 정보 활용 (request.auth)

     

    request.auth란? 
    Firebase Auth를 사용하여 인증되었다고 가정하는 사용자 정보를 포함한다.

     

    request.auth.uid  : 사용자 uid 정보 

    request.auth.token.email :  사용자 email 정보 
    request.auth.token.email_verified : 전자메일 주소의 확인여부. 
    request.auth.token.email.matches('.*@domain[.]com') : 전자메일 도메인 종류 
    .matches('.*google[.]com$') : Google.com을 사용한 사람들만 읽도록 하게 하고 싶다. 

    request.auth.token.phone_number : 휴대폰 인증 시 휴대폰 번호 
    request.auth.token.firebase.sign_in_provider == "phone" : 인증방법 
    (custom, password, phone, anonymous, google.com, facebook.com, github.com, twitter.com)


    https://firebase.google.com/docs/reference/rules/rules.firestore.Request

    예시 )

    allow read : if request.auth.token.email != null && request.auth.token.email_verified 

    단, 이메일 인증을 사용하지 않을 경우 이메일 인증 관련 보안규칙을 적으면 오류가 발생한다.

    또한 휴대폰 인증서비스에 대한 보안규칙 사용 시 Authentication에 저장된 형태인 phone_number == '821045241423' 형태만 허용가능하다. '01045241423' 형태는 같지 않다고 인식하니 유의하자.

     

     

    - 사용자 인증 클레임 생성

     

    서버 측 라이브러리 또는 cloud function을 사용하여 특정 사용자에 대해 설정하는 사용자 지정 변수이다.

    match / {everythingInMyDatabase == **}{
    allow read, write : if request.auth.token.super_admin == true;
    }
    
    match /reviews / {reviewID}{
    allow read, write : if request.auth.token.role == "moderator";
    }

     

     

     

     

     

    문서의 엑세스에 대한 보안조건

     

    불필요한 내용의 데이터가 포함되는 것을 막기 위해 사용한다.

     

    데이터의 종류 

    - request data : the data requests that's coming in

    - target documents : you're looking to either read or update ( resource.data )

    - some other document : some other data located in some other part

     


     

    - 사용자가 작성한 문서를 데이터베이스에 쓸 경우 (request.resource.data)

     

    request.resource.data란?
    사용자가 write하기를 시도하는 document의 모든 필드를 표현한다.

     

    //작성하고자 하는 문서의 score 필드값이 50일 때 쓰기 허용
    allow write : if request.resource.data.score == 50
    allow write : if request.resource.data["score"] == 50
    //score 필드의 값이 숫자임을 확신할 수 있으며 null value를 허용하지 않는다
    allow write : if request.resource.data.score is number 
    //score 필드 값이 1보다 크고 5보다 작은 문서만 쓸 수 있도록 한다.  
    allow write : if request.resource.data.score >= 1 && request.resource.data.score <= 5
    
    //score 필드값이 1보다 크거나 5보다 작은 문서를 쓸 수 있도록 한다.
    allow write : if request.resource.data.score >= 1 
    allow write : if request.resource.data.score <= 5
    //작성하고자하는 문서의 category의 내용이 [widgets, things]내에 있을 경우만 허용  
    allow write : if request.resource.data.category in ['widgets', 'things']
    //headline이 string type이고 200 character 미만일 때만 쓸 수 있도록 한다.
    allow write : if request.resource.data.headline is string && request.resource.data.headline.size() <200 
    //firestore에 추가되고자 하는 문서의 reviewID 필드값과 request.auth.uid 같을 때만 쓸 수 있도록 허용한다. 
    allow write : if request.resource.data.reviewID == request.auth.uid; 

     

     

     

    - 이미 데이터베이스에 존재하는 문서를 읽거나 쓸 경우(resource.data)

     

    resource.data란?
    이미 데이터베이스에 있는 문서를 나타내는 것을 의미한다.

     

    //지정한 문서의 reviewerID 필드와 request.auth.uid이 같을 때만 문서를 수정할 수 있도록 한다.
    allow update: if resource.data.reviewerID == request.auth.uid 
    //score의 필드값을 변화시키지 않기 위함. 
    allow update: resource.data.score == resource.data.score;
    // 지정문서의 x 필드의 값이 5 이상인 데이터만 사용자가 읽도록 하기 위함
     allow read: if resource.data.x > 5; 
    // 지정문서의 published 필드값이 true인 데이터만 사용자가 읽도록 하기 위함 
    allow read: if resource.data.published == true ; 
    allow read: if resource.data.name == 'John Doe'

     

     

     

    - 다른 문서에 대한 엑세스 (지정한 문서 외에 다른 문서를 사용해야할 경우)

     

    get : 지정 문서 외에 다른 문서를 가져와서 비교

    exist : 다른 문서에 데이터 존재 여부를 알기 위함

     

    exist 예시 )

    //users collection내에 특정 uid의 문서가 존재하는가  
    
    if exists(/databases/$(database)/documents/users/$(request.auth.uid))
    if exists(/databases/$(database)/documents/Users/$('2s6xdCuygpMv0ralWTnxbRZcYo73'));
    
    //users subcollection내에 request.auth.uid인 문서가 존재하는가  
    
    if exists(/databases/$(database)/documents/leagues/$(league)/users/$(request.auth.uid))} 
    //Users 컬렉션 내 특정문서의 'name' 필드를 write하려고 한다. 
    //새로운 name의 필드값이  Kits의 document id에 존재할 때만 허용한다.
    
     match /Users/{user} {
     
    		allow write :
            if exists(/databases/$(database)/documents/Kits/$(request.resource.data.name));
            
    }
    //Users 컬렉션 내 특정 문서의 name 필드를 read 혹은 write하려고 한다. 
    //write하고자 하는 문서의 birth 필드값이 Kits의 document id로 존재할 때 허용한다. 
    
    match /Users/{user} {
    
    	allow read, write :
        if exists(/databases/$(database)/documents/Kits/$(resource.data.birth));
        
        }

    *request.resource.data는 사용자가 데이터베이스에 write하기를 시도하는 데이터임으로 read에는 적용할 수 없다. 반면 resource.data는 현재 데이터베이스에 존재하는 데이터임으로 read에 적용할 수 있다.

     

     

    get 예시)

    //request.auth.uid인 문서의 admin 필드가 true인가    
    if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
    //map 형태의 필드값을 가져올 때
    //(roles / request.auth.uid / role / isadmin : true) 일때 승인
    
    
    match /posts/{docId} {    
        allow read, write: if hasRole("isAdmin")  
    }      
    function hasRole(userRole) {    
      return get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.role[userRole] 
    }
    //map 형태의 필드값을 가져올 때
    //(restaurants/restaurantID/ private_data (sub collection) / private / roles /request.auth.uid 
    //== editor or owner ) 일 때 승인
    
    
    if get(/databases/$(database)/documents/restaurants/$(restaurantID)
    /private_data/private).data.roles[request.auth.uid] == ["editor" ,"owner"]

     

    * $(database)는 Cloud Firestore Security Rules 와일드 카드로 사용할 수 있는 데이터베이스 인스턴스를 정의하는 매개변수를 의미한다. match /databases/{database}/documents

     

    *exist는 다른 문서에 특정 문서가 존재하는지를 판별한다. 특정 필드값이 존재하는지 확인하기 위해서는 get을 사용한다.

     

     

     

    - 쿼리한 내용에 대한 데이터를 가져올 경우 ( request.query )

     

    limit - query limit clause.
    offset - query offset clause.
    orderBy - query orderBy clause.

     

    document fusersQuery = Firestore.instance.collection('users').orderBy('lastLaunch').limit(20)
    fusersQuery.getDocuments()
    allow list; if request.query.limit <= 20 && request.query.orderBy.lasetLaunch == "ASC";

     

     

     

    - 사용자가 요청한 시간에 따라 보안규칙을 적용할 경우 (request.time)

    //요청한 시간이 2019년 3월 24일 이후 일때만 읽기 허용
    allow read: if request.time.toMillis() >=
                 timestamp.date(2020,1,1).toMillis(); 
    //데이터 베이스에 저장된 publication timestamp 와 요청된 시간 비교 
    // 1 millisecond = 0.001 seconds.
    
    allow read: if request.time.toMillis() >=
                resource.data.publicAt.seconds * 1000;

     

     

     

    - time에 대한 조건을 생성할 경우 (duration) 

     

    duration.time(hours, mins, secs, nanos)

     //마지막으로 전송된 알람시간 이후 1시간이 지난 문서만 read할 수 있다.
     
    allow read: if request.time > (resource.data.lastSendNotification + duration.time(1, 0, 0, 0));

     

    duration.value(간격, 단위)

    w Weeks
    d Days
    h Hours
    m Minutes
    s Seconds
    ms Milliseconds
    ns Nanoseconds
    match /notification/{notificationId} {
       allow create: if 
          request.time.toMillis() > getRequiredTimeInMillis();
                
       function getRequiredTimeInMillis() {
         return (getLastSendNotificationTimestamp().seconds +
                       duration.value(2, 'h').seconds()) * 1000;
       }
                 
       function getLastSendNotificationTimestamp() {
         return get(/databases/$(database)/documents/
            user/$(request.auth.uid)).data.lastSendNotification;
       }
    }
    
    //마지막으로 전송된 알람시간 이후 2시간이 지나야 문서를 create할 수 있다.

    https://firebase.google.com/docs/reference/rules/rules.duration_

     

     

     

    - 함수 사용

    반복적으로 사용하는 규칙의 경우 함수로 만들어놓으면 편리하다.

    service cloud.firestore {
    
      match /databases/{database}/documents {
     
     //지정문서범위 바깥 쪽에 있는 함수
     	 function outerauthorOrPublished(storyid) {
            return resource.data.published == true || request.auth.uid == 	resource.data.author;
          }
         match /stories/{storyid} {
        
         //지정문서범위 내에 있는 함수
             function innerauthorOrPublished() {
               return resource.data.published == true || request.auth.uid == 	esource.data.author;
             }
    
            
             allow list: if request.query.limit <= 10 &&
                             innerauthorOrPublished();
        
             allow get: if outerauthorOrPublished(storyid); 
             allow write: if request.auth.uid == resource.data.author;
            }
          }
        } 

     

     

     

    - 반드시 주의해야할 점

     

    보안규칙은 필터가 아니다. 실제 데이터에 접근하는 것이 아니라 코드를 통해 가정된 데이터에 보안규칙을 적용하기 때문에 쿼리한 문서에 대해 보안규칙을 적용할 경우 코드 상에서의 쿼리 내용과 보안규칙에서의 쿼리 내용이 동일해야한다. 코드가 아닌 보안규칙에만 쿼리를 적용하면 조건에 맞지 않는 데이터가 들어올 수 있기 때문에 보안규칙이 해당 데이터를 거부할 수 있다.

    allow read, write: if request.auth.uid == resource.data.author;

     

    올바른 예시)

    FirebaseUser user = await _firebaseAuth.currentUser();
    Firestore.instance.collection("stories").where("author", isEqualTo: user.uid)
             .getDocuments()  

     

    잘못된 예시) 해당 쿼리를 가져올 수 없다.

    FirebaseUser user = await _firebaseAuth.currentUser();
    Firestore.instance.collection("stories").getDocuments()
    반응형

    댓글

Designed by Tistory.