13

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.

4
  • 2
    Please add your rules and code as text instead of an image. Commented Aug 26, 2018 at 12:35
  • Also, there are some functions called within your rules which aren't shown in your image Commented Aug 26, 2018 at 18:05
  • Hi @JasonBerryman, I've updated the whole post, and put all the rules. Commented Aug 26, 2018 at 22:17
  • @AndréKool, I've now used plain text. Commented Aug 26, 2018 at 22:17

1 Answer 1

25

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
}
Sign up to request clarification or add additional context in comments.

1 Comment

Only using resource.data.uid was the correct solution for me as well. I was also trying to check the request. This solution good.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.