Selast Lambou
Selast Lambou

Reputation: 748

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:

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.

Upvotes: 13

Views: 4596

Answers (1)

Dan B.
Dan B.

Reputation: 1583

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
}

Upvotes: 25

Related Questions