Sails permissions by example

The goal of this post is to provide a hands on example of the sails-permissions library. Note that this example is based on version 1.x.x of sails-permissions. You can start from scratch, or you can check out the project with some initial setup, or in its finished state. If you just want to check out the project in it's finished state:

git clone git@github.com:ryanwilliamquinn/sails-permissions-example.git  
cd sails-permissions-example  
git ch -f complete  
npm install  

Sails-permissions is all about managing which users can perform which actions on which models.

For our example application, we will create an API for a reviews site.

The permissions rules are as follows:

  • Anyone can read an active review.
  • To create a review, a user must be logged in.
  • To edit or delete a review, a user must be logged in and must be the owner of the review.
  • An admin user can create/read/update/delete any review, regardless of ownership.

To start off, you can either check out the code from github in its initial state, which has sails-permissions installed configured, and the models set up:

git clone git@github.com:ryanwilliamquinn/sails-permissions-example.git  
cd sails-permissions-example  
git ch -f step1  
npm install  

If you are using the cloned repository, skip ahead to creating permissions

Otherwise you can follow the manual installation instructions to learn how to set up sails permissions:

Step 1: create a new sails app

sails new reviews  
cd reviews  
npm install  
npm install --save lodash  

Step 2: install sails-permissions and sails-auth

npm install --save sails-permissions sails-auth  

2a: Add the sails permission generator configuration to .sailsrc in the root of the sails app (make your .sailsrc file look like this):

{
  "generators": {
    "modules": {
      "permissions-api": "sails-permissions/generator"
    }
  }
}

2b: Run the generator

sails generate permissions-api  
## should get some output like: info: Created a new permissions-api ("permissions-api")!

2c: Optionally set environment variables for the admin user.
We will be using the defaults for this example, but in a production app you should definitely change them.
The env vars are: ADMIN_USERNAME, ADMIN_EMAIL, and ADMIN_PASSWORD

2d: Update the policies configuration. Make your config/policies.js file look like this:

module.exports.policies = {  
  '*': [
    'basicAuth',
    'passport',
    'sessionAuth',
    'ModelPolicy',
    'AuditPolicy',
    'OwnerPolicy',
    'PermissionPolicy',
    'RolePolicy',
    'CriteriaPolicy'
  ],

  AuthController: {
    '*': ['passport']
  }
};

Step 3: create the 'review' model and controller

sails generate api review  

3a: Add some fields to the 'review' model - add this block to the 'attributes' section of api/models/Review.js

title: 'string',  
text: 'string',  
category: 'string'  

Step 4: Some configuration for the ORM - uncomment this line from config/models.js:

migrate: 'alter'  

Step 5: Make sure it works:

sails lift  
## this should start up the app
## you should be able to browse to http://localhost:1337 and see the sails splash page
## hit ctrl-c a couple times to stop the app once you have verified that it is working
Creating Permissions


There is a permissions admin UI in the works, but for now the easiest way to examine permissions is through the sails repl. Now that our project is configured, we can start it up via sails console to bring up the repl.

Sails-permissions has a few default roles ('admin', 'registered', 'public'). We can see them by running this command in the repl:

Role.find().exec(console.log)  

We can see the relevant users and permissions for the admin role by running this in the repl:

Role.find({name:'admin'}).populate('users').populate('permissions').exec(console.log)  

You can see that the admin role has crud permissions for all of the relevant models. This query also lists the users that have this role, which should only be the user named 'admin' (assuming you are using the default admin credentials).

So, back to our reviews site. By default, the admin role has crud permissions on the Review model. We can test this via curl. You can also use postman to interact with the api we are creating. In that case you would just have to log in with the appropriate user. To log into a sails-permissions app (or a sails-auth app) using username and password, post to localhost:1337/auth/local with the fields identifier=admin and password=admin1234. Anyway, we will carry on using curl and basic auth. Assuming you are using the default admin credentials, you can use basic auth to create a new Review:

curl -H "Authorization: Basic YWRtaW46YWRtaW4xMjM0" -X POST "http://localhost:1337/review" -d "title=iphone 8&text=so much better&category=smartphones"  

If you used something other than the default credentials, just replace the base 64 encoded bit (after "Authorization: Basic") with the base 64 encoded version of yourusername:yourpassword.

Note that if you don't include the basic authorization, you will not be allowed to create a review:

curl -X POST "http://localhost:1337/review" -d "title=iphone 8&text=so much better"  
## should fail with message: You are not permitted to perform this action.

Next step is to create some users. We can do this easily with curl and our admin user credentials:

## create user travis
curl -H "Authorization: Basic YWRtaW46YWRtaW4xMjM0" -X POST "http://localhost:1337/user" -d "username=travis&email=travis@theemail.com&password=secretpassword"

## create user venise
curl -H "Authorization: Basic YWRtaW46YWRtaW4xMjM0" -X POST "http://localhost:1337/user" -d "username=venise&email=venise@theemail.com&password=password123"  

Our new users, will be part of the registered role by default, but will not be part of the admin role unless they are specifically added. You can verify this via curl or the repl.

In our app registered users should be allowed to create content, so let's create that Permission via the repl. There are a number of helper functions in the PermissionsService, see here for details. One of them is 'grant', which creates a new permission, and associates the permission with a role. Run this in the repl:

PermissionService.grant({action: 'create', model: 'review', role: 'registered'})  

Now travis or venise should be able to create a review:

## travis
curl -H "Authorization: Basic dHJhdmlzOnNlY3JldHBhc3N3b3Jk" -X POST "http://localhost:1337/review" -d "title=99 honda civic&text=still works after all these years&owner=2&category=cars"

## venise
curl -H "Authorization: Basic dmVuaXNlOnBhc3N3b3JkMTIz" -X POST "http://localhost:1337/review" -d "title=pontiac grand am&text=so much pontiac&owner=3&category=cars"  

We set the owner field explicitly (to the id of travis or venise respectively), because the creator of an object is not necessarily the owner.

Note that we only granted create permissions, registered users still can't read reviews:

## travis
curl -H "Authorization: Basic dHJhdmlzOnNlY3JldHBhc3N3b3Jk" "http://localhost:1337/review"  
## "error": "User travis@theemail.com is not permitted to GET "

Logged in (registered role) users should be able to read reviews, so lets grant 'read' to the registered role (TODO - why isnt public working here?):

PermissionService.grant({action: 'read', model: 'review', role: 'registered'})  

And now travis and venise should be able to browse reviews:

## travis
curl -H "Authorization: Basic dHJhdmlzOnNlY3JldHBhc3N3b3Jk" "http://localhost:1337/review"  
## venise
curl -H "Authorization: Basic dmVuaXNlOnBhc3N3b3JkMTIz" "http://localhost:1337/review"  

Sails-permissions has a notion of ownership. Registered users should only be allowed to update reviews that they own.

PermissionService.grant({action: 'update', model: 'review', role: 'registered', relation:'owner', criteria: {blacklist: ['category']} })  

Now travis can update his review:

curl -H "Authorization: Basic dHJhdmlzOnNlY3JldHBhc3N3b3Jk" -X PUT "http://localhost:1337/review/2" -d "title=95 honda civic"  

But venise can't update travis' review:

curl -H "Authorization: Basic dmVuaXNlOnBhc3N3b3JkMTIz" -X PUT "http://localhost:1337/review/2" -d "title=venise was here"  

And travis may not update a review that he isn't the owner of:

curl -H "Authorization: Basic dHJhdmlzOnNlY3JldHBhc3N3b3Jk" -X PUT "http://localhost:1337/review/1" -d "title=travis was here"  

There is also a blacklisted attribute for the update permission - 'category'. This means registered users may not update the 'category' attribute (even on a review they own). If they try, the whole update will fail. Here is an example with travis' credentials:

curl -H "Authorization: Basic dHJhdmlzOnNlY3JldHBhc3N3b3Jk" -X PUT "http://localhost:1337/review/2" -d "title=89 honda civic&category=restaurant"  
## this should fail with a message like:
## "error": "Can't update, because of failing where clause or attribute permissions"

Blacklisted attributes work the same way with 'create' permissions. If a permission has a blacklisted attribute, and you try to create a model with that attribute set in the body of the request, the create will fail. If a 'read' permission has one or more blacklisted attributes, those attributes will not be returned in the response.

Lets create another role, and call it carsCategoryAdmin. This role will be similar to the admin role, but only pertinent for the cars category. We can use the PermissionService helper method to create this role, and add the permissions and users via the repl. In this case we are creating the role, adding update and delete permissions for all Review objects, and adding venise to the role:

PermissionService.createRole({name: 'carsCategoryAdmin', permissions: [{ action: 'update', model: 'review', criteria: [{ where: { category: 'cars'}}]}, { action: 'delete', model: 'review', criteria: [{ where: { category: 'cars'}}]}], users: ['venise']})  

We can examine the role and related permissions and users via the repl:

Role.find({name: 'carsCategoryAdmin'}).populate('users').populate('permissions').exec(console.log)  

Now, as a member of the carsCategoryAdmin role, venise can update any review in the cars category, even one she does not own:

curl -H "Authorization: Basic dmVuaXNlOnBhc3N3b3JkMTIz" -X PUT "http://localhost:1337/review/2" -d "title=89 honda civic"  

She still can't update reviews she doesn't own in other categories:

curl -H "Authorization: Basic dmVuaXNlOnBhc3N3b3JkMTIz" -X PUT "http://localhost:1337/review/1" -d "title=cant wait for iphone 9"  

That should cover the basics of interacting with sails permissions. If you have questions or comments, let me know at ryanwilliamquinn@gmail.com