Vivek Athalye
Vivek Athalye

Reputation: 2979

firebase rules: how to restrict access based on user role

I'm new to Firebase and trying to understand the security rules. For this I'm implementing typical functionality of Project - Team Members - Tasks. Each project will have a team leader, multiple members and multiple tasks.

Here is the structure and rules that I'm trying to implement (a.k.a. Requirements):

/Members - each member has { displayName, email, emailVerified }
    any logged in user should be able to read data from Members (to get the 
        display names of all users)
    any logged in user should be able to update his/her record

/Projects - each project has { Lead, Members{}, Name, Tasks{} }
    any logged in user should be able to read the list of projects
    any logged in user should be able to read the list of members (if possible
        only for the projects where they are part of)
    any logged in user should be able to read the list of tasks (if possible only 
        for the projects where they are part of)
    only the team leader should be able to update project details i.e.
        - add / remove members
        - add / remove tasks
        - change project title

/Tasks - { project, status, title }
    team leader / team members should be able to read the tasks
    team leader can add/edit/delete tasks
    team members can update only status (of a task that is associated with their project)
    team leader / team members should be able to filter project tasks based on 
    task status (completed / not completed)

I've setup following Firebase rules:

{
 "rules": {
    "Members": {
        ".read": "auth != null",
        "$mid" : {
            ".write": "auth != null && auth.uid == $mid"
        }
    }, // Members
    "Projects": {
        ".read": "auth != null",
        // only team lead can edit project details
        ".write": "auth != null && auth.uid == data.child('Lead').val()",
        // Lead and Name are mandatory fields
        ".validate": "newData.hasChildren(['Lead', 'Name'])",
        "Name": {
            ".validate": "newData.isString() && newData.val().length > 0"
        },
        "Lead": {
            ".validate": "root.child('Members/' + newData.val()).exists()"
        },
        "Members": {
            "$mid": {
                ".validate": "root.child('Members/' + $mid).exists()"
            }
        },
        "Tasks": {
            "$tid": {
                ".validate": "root.child('Tasks/' + $tid).exists()"
            }
        }
    }, // Projects
    "Tasks": {
        // allow read / write only if current user is team lead or a member of the project
        ".read": "(auth != null) && (data.child('project').val() == 'Project1')",
        ".write": "auth != null && ( root.child('Projects/' + newData.child('project').val() + '/Lead').val() == auth.uid || root.child('Projects/' + newData.child('project').val() + '/Members/' + auth.uid).exists() )",
        ".validate": "newData.hasChildren(['project', 'status', 'title'])",
        "status": {
            ".validate": "newData.isString() && newData.val().length > 0"
        },
        // if team member is saving the item, allow changes only to status
        "title": {
            ".validate": "(root.child('Projects/' + newData.parent().child('project').val() + '/Lead').val() == auth.uid) ? newData.isString() && newData.val().length > 0 : data.exists() && data.val() == newData.val()"
        }
    } // Tasks
 } // rules
}

Currently I'm evaluating the .read functionality. I've not tested .write functionality yet.

I'm able to get Members list (members' displayName) for a given project. But while getting project's task details (from /Tasks) I'm getting permission denied error.

Note that I would like to use .read rule same as .write rule for Tasks. But as I was getting error, I changed it to the current rule (such that, any authenticated user can read the tasks for Project1 - Project1 is the key of a project). Even then I'm getting permission denied. If I keep only "auth != null" then I'm able to read the tasks but that's not what I want.

Can someone help me understand what changes should I make to the Firebase rules so as to implement the requirements given above?

Upvotes: 2

Views: 1124

Answers (1)

Vivek Athalye
Vivek Athalye

Reputation: 2979

After trying out different combinations here is what I've found out.

I was trying to access /Tasks with orderByChild('project').equalTo(projectKey) so as to get details of tasks associated with a project. But when I do this, the .read rule gets executed at /Tasks level and there is no child named 'project' at that level. 'project' is available at /Tasks/<taskId>/project. So I need to change the Task rule as:

"Tasks": {
    "$tid": {
        // allow read / write only if current user is team lead or a member of the project
        ".read": "auth != null && ( root.child('Projects/' + data.child('project').val() + '/Lead').val() == auth.uid || root.child('Projects/' + data.child('project').val() + '/Members/' + auth.uid).exists() )",
        ".write": "auth != null && ( root.child('Projects/' + newData.child('project').val() + '/Lead').val() == auth.uid || root.child('Projects/' + newData.child('project').val() + '/Members/' + auth.uid).exists() )",
        ".validate": "newData.hasChildren(['project', 'status', 'title'])",
        "status": {
            ".validate": "newData.isString() && newData.val().length > 0"
        },
        // if team member is saving the item, allow changes only to status
        "title": {
            ".validate": "(root.child('Projects/' + newData.parent().child('project').val() + '/Lead').val() == auth.uid) ? newData.isString() && newData.val().length > 0 : data.exists() && data.val() == newData.val()"
        }
    }
} // Tasks

Even with this rule accessing /Tasks with orderByChild('project').equalTo(projectKey) gives permission denied. This is because now there is no .read rule defined at /Tasks level. So I need to change the program logic to iterate on /Projects/<projectId>/Tasks and for each taskId access /Tasks/<taskId>. When I do this, the .read rule gets evaluated properly and user can access tasks details only for the projects that they are part of. And then I need to process these task details at the client side to separate out completed and non-completed tasks.

I'm yet to verify .write and .validate rules. But meanwhile I'll wait for someone to confirm my understanding or correct it.

Upvotes: 2

Related Questions