Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firestore: Delete document and security rules

Description

I have a problem dealing with deletion using firestore. In short, I created a security rule for posts like this:

First there are some functions in the rules:

service cloud.firestore {

function userRoles() {
    return ['admin', 'customer', 'reader'];
}

function userGenders() {
    return ['mal', 'female', 'other'];
}

function postVisibilities() {
    return ['public', 'private', 'protected'];
}

function postType() {
    return ['music', 'motion_design', 'graphic_art'];
}

function isPayment(paymentDoc) {
    return paymentDoc != null
        && paymentDoc.date is timestamp
        && paymentDoc.price is number
        && paymentDoc.price is number
        && paymentDoc.price > 0;
}

function isBill(billDoc) {
    return billDoc.sellerId is string
        && billDoc.buyerId is string
        && billDoc.postIds != null
        && billDoc.date is timestamp
        && billDoc.paymentDoc != null
        && isPayment(billDoc.paymentDoc);
}

function isAccount(accountDoc) {
    return accountDoc.isRegistered is bool
        && accountDoc.addressId is string
        && accountDoc.contactId is string
        && accountDoc.email is string
        && accountDoc.username is string
        && accountDoc.gender is string
        && accountDoc.gender in userGenders()
        && accountDoc.role is string
        && accountDoc.role in userRoles();
}

function isPost(postDoc) {
    return postDoc.createdAt is timestamp
        && postDoc.updatedAt is timestamp
        && postDoc.title is string
        && postDoc.text is string
        && postDoc.image is string
        && postDoc.authorId is string
        && postDoc.visibility is string
        && postDoc.visibility in postVisibilities();
}

function isVote(voteDoc) {
    return voteDoc.authorId is string
        && voteDoc.reaction is string
        && voteDoc.reaction in ['up', 'down'];
}

function isComment(commentDoc) {
    return commentDoc.authorId is string
        && commentDoc.message is string;
}

function isSingle(doc) {
    return doc.size() == 1;
}

match /databases/{database}/documents {

    function userExists(userId) {
        return userId != null && exists(/databases/$(database)/documents/accounts/$(userId));
    }

    function getUserRole(userId) {
        return get(/databases/$(database)/documents/accounts/$(userId)).data.roles;
    }

    function hatUserRole(userId, role) {
        return getRoleForUser(userId) in role;
    }

    match /{document=**} {
        allow read: if true;
        allow write: if false;
    }

    match /accounts/{accountId} {

        allow create: if isAccount(request.resource.data)
                            && (request.auth.uid == accountId || hatUserRole(request.auth.uid, ['admin']));
        allow update: if request.auth.uid == accountId || hatUserRole(request.auth.uid, ['admin']);
        allow delete: if hatUserRole(request.auth.uid, ['admin']);

        match /contacts/{contactId} {
            allow write: if isSingle(request.resource.data)
                        && request.auth.uid == accountId;
            allow read: if userExists(request.auth.uid);
        }

        match /favorites/{favoriteId} {
            allow write: if isSingle(request.resource.data)
                        && request.auth.uid == accountId;
            allow read: if userExists(request.auth.uid);
        }

        match /votes/{voteId} {

            allow create: if isVote(request.resource.data)
                        && userExists(request.auth.uid);
            allow update: if userExists(request.auth.uid)
                        && isVote(request.resource.data)
                        && request.resource.data.authorId == request.auth.uid
            allow delete: if userExists(request.auth.uid)
                        && (request.auth.uid == accountId
                        || hatUserRole(request.auth.uid, ['admin']))
        }
    }

    match /bills/{billId} {
        allow create: if isBill(request.resource.data)
                    && userExists(request.resource.data.sellerId)
                    && userExists(request.resource.data.buyerId)
                    && (request.resource.data.buyerId == request.auth.uid
                    || request.resource.data.sellerId == request.auth.uid);
        allow update, delete: if false;
        allow read: if request.resource.data.buyerId == request.aut.uid
                        || request.resource.data.sellerId == request.aut.uid;
    }

    match /posts/{postId} {

        function publicPost() {
              return get(/databases/$(database)/documents/posts/$(postId)).data.visibility == 'public';
        }

        function postVisibility() {
            return get(/databases/$(database)/documents/posts/$(postId)).data.visibility;
        }

        function protectedPost() {
            return userExists(request.auth.uid)
                && get(/databases/$(database)/documents/posts/$(postId)).data.visibility == 'public';
        }

        function findPostAuthor(pathToFind) {
                return get(/databases/$(database)/documents/posts/$(pathToFind)).data.authorId
        }

        allow create, update: if isPost(request.resource.data)
                    && userExists(request.auth.uid)
                    && request.resource.data.authorId == request.auth.uid;
        allow read: if request.resource.data.visibility == 'public';
        allow delete: if userExists(request.auth.uid)
                    && findPostAuthor(request.resource.id) == request.auth.uid;

        match /votes/{voteId} {
            allow read: if protectedPost(postId)
                        || publicPost(postId);
            allow create: if isVote(request.resource.data)
                        && postVisibility(postId) in ['public', 'protected']
                        && userExists(request.auth.uid);
            allow update: if isVote(request.resource.data)
                        && request.resource.data.authorId == request.auth.uid;
            allow delete: if userExists(request.auth.uid)
                        && (request.auth.uid == request.resource.data.authorId
                        || hatUserRole(request.auth.uid, ['admin']));
        }

        match /comments/{commentId} {
            allow read: if protectedPost(postId) || publicPost(postId);
            allow create: if isComment(request.resource.data)
                        && postVisibility(postId) in ['public', 'protected']
                        && userExists(request.auth.uid);
            allow update: if isComment(request.resource.data)
                        && request.resource.data.authorId == request.auth.uid;
            allow delete: if userExists(request.auth.uid)
                        && (request.auth.uid == request.resource.data.authorId
                        || hatUserRole(request.auth.uid, ['admin']));
        }
    }
}
}

For creation and update, all work fine.

After that, I created two methods implementations to delete a document, the two of them using ids which are:

public deletePost(postId: string): Observable<void> {
    const postRef = this.db.collection('posts').doc(postId).ref;

    return fromPromise(this.db.firestore.runTransaction((transaction => {
        return transaction.get(postRef).then(snapshot => {
            if (!snapshot.exists) {
                this.snackBar.open('Post doesn\'t exist', 'close');
            } else {
                const auth = snapshot.data().authorId === this._userId;
                if (auth) {
                    transaction.delete(postRef);
                } else {
                    this.snackBar.open('You\' not allowed to do that!');
                }
            }
        });
    })));
}

using transactions, and:

protected removeElement(elementId: string): Observable<any> {
    return fromPromise(this.db.collection(this.dbCollection).doc(elementId).delete());
}

not using any transaction, simple deletion.

Problem

Neither of those method work.

When using the first method, I get:

ERROR Error: Server responded with status 
at new FirestoreError (index.cjs.js:346)
at T.<anonymous> (index.cjs.js:6901)
at Ab (index.js:23)
at T.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.dispatchEvent (index.js:21)
at te (index.js:66)
at ve (index.js:69)
at T.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.jb (index.js:67)
at T.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.Na (index.js:67)
at XMLHttpRequest.wrapFn (zone.js:1188)
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421)
at Object.onInvokeTask (core.js:3815)
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:420)
at Zone.push../node_modules/zone.js/dist/zone.js.Zone.runTask (zone.js:188)
at ZoneTask.push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask [as invoke] (zone.js:496)
at invokeTask (zone.js:1540)
at XMLHttpRequest.globalZoneAwareCallback (zone.js:1566)

and I get with the second one:

ERROR Error: Missing or insufficient permissions.
at new FirestoreError (index.cjs.js:346)
at index.cjs.js:7088
at W.<anonymous> (index.cjs.js:7033)
at Ab (index.js:23)
at W.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.dispatchEvent (index.js:21)
at Re.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.Re.Ca (index.js:98)
at ye.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.Oa (index.js:86)
at dd (index.js:42)
at ed (index.js:39)
at ad (index.js:37)
at L.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.Sa (index.js:36)
at L.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.nb (index.js:35)
at Ab (index.js:23)
at T.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.dispatchEvent (index.js:21)
at ve (index.js:68)
at T.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.jb (index.js:67)
at T.push../node_modules/@firebase/webchannel-wrapper/dist/index.js.g.Na (index.js:67)
at XMLHttpRequest.wrapFn (zone.js:1188)
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:421)
at Object.onInvokeTask (core.js:3815)
at ZoneDelegate.push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (zone.js:420)
at Zone.push../node_modules/zone.js/dist/zone.js.Zone.runTask (zone.js:188)
at ZoneTask.push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask [as invoke] (zone.js:496)
at invokeTask (zone.js:1540)
at XMLHttpRequest.globalZoneAwareCallback (zone.js:1566)

Since I'm not sure if the problem where the problem is, I have some theories:

  • Maybe in the rules, when I write

    allow delete: if userExists(request.auth.uid) && findPostAuthor(request.resource.data.id) == request.auth.uid;

    I think, that as I only looking directly at the document using it's Id, the request.resource.data.id mustn't contain anything.

  • I also thought, that if transactions doesn't work, it may be because the way it actually works is really different from what we see on other transactions functions.

As I'm using angularFire2, this.db => AngularFirestore, this.dbCollection => 'posts' and in the structure of any post, there is a autorId field which is a string.

like image 781
Selast Lambou Avatar asked Aug 26 '18 08:08

Selast Lambou


Video Answer


1 Answers

For delete, you need to compare request.auth.uid to resource.data.uid, not to request.resource.data.uid. For example:

match /bookmarks/{id} {
  allow create: if request.resource.data.uid == request.auth.uid
  allow read, delete: if resource.data.uid == request.auth.uid
}
like image 180
Dan B. Avatar answered Oct 12 '22 13:10

Dan B.