Policies

It is not uncommon to restrict access to different routes based on who is making the request. For example, you many want to restrict access to a route based a request's origin, IP address, or if the request has the correct authorizations.

Blueprint realizes this need through its policy framework. In this part of the tutorial, we will explore how to create a simple policy, attach it to a route, and then write a unit test to test the policy.

Defining a policy

Let's say we want to restrict access to GET /api/rentals to requests that only have the header Secret-Key and the value of the header is set to ssshhh.

This example is for demonstration purposes only. Please do not do anything this simple in a production environment when it comes to securing an API.

To do this, we are going to create a policy as shown below. As shown in the example, our policy is first placed in the /app/policies directory. We then organize our policy under the /rental directory since this policy pertains to the rental resource. Lastly, we place the policy in a file named getAll.js. This help us remember the policy is for getting all rentals (more on this later).

// app/policies/rental/getAll.js.js
const { Policy } = require ('@onehilltech/blueprint');

/**
 * This is a simple demonstration of a policy.
 */
module.exports = Policy.extend ({
   /// The failure code used when the policy fails.
   failureCode: 'invalid_secret',
   
   /// The human readable message that can be displayed on the 
   /// client-side when this policy fails.
   failureMessage: 'The request has an invalid secret.',
   
   /**
    * Run the policy.
    *
    * @param req     The Express request object.
    */
   runCheck (req) {
     // Check the Secret-Key request header, but do not use something like this in
     // a production environment.
     
     return req.get ('Secret-Key') === 'ssshhh';
   }
 }); 

Each policy must implement the runCheck(req) method because this is the main entry point the framework uses to execute a policy. If the policy succeeds, it must return true. If the policy fails, it can return either false, or a failure object. The failure object is a hash that contains a failureCode and failureMessage property. As you see in the example above, the policy can also define a default failureCode and failureMessage at the top-level of the policy. The default failure code and failure message are used when the policy returns false.

In the example above, we are checking the request header for the value of the Secret-Key header. If the value is ssshhh, the policy returns true. If the value is not ssshhh, then the policy returns false.

Policies that return true are allowed to continue its routing process either to the next policy, or to the controller action for the route. Policies that return false stop the request handling, and return 403 to the client. The body of the response will contain the corresponding failureCode and failureMessage associated with the failed policy.

Attaching the policy to a route

Now that we have defined a policy, we need to attach (or bind) the policy to the correct route. We do this in the router specification.

Let's assume that we go back to the router we defined before we started using resources. This router is illustrated below. The router has a policy property that is used to define the policy for a route or an action. As shown below, we have specified the policy created above on the /rentals route defined below.

// app/routers/api/rental.js

const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/rentals': {
      // This policy applies to all requests /api/rentals regardless of the
      // HTTP verb (or action) called.
      policy: 'rentals.getAll',
      
      get: {
        // If we add the policy here, then it only applies to GET /api/rentals.
        policy: 'rentals.getAll', 
        
        // This statement will bind this route to the get action in the 
        // rental controller. Now, GET /api/rentals can handle client requests.
        action: 'rental@getAll'
      }
    }
  }
});

Policies for resources

The example above illustrated how to manually attach a policy to a route. This works when you manually bind routes to actions in a router specification. When we use resources, however, we have to use a different approach. Instead, the framework automatically attaches policies to resource routes. This is done by searching for a policy that matches resource name + action.

If you recall, we defined the policy for our example in a file named /app/policies/rental/getAll.js. We did this in preparation for replacing the manually specification of a policy with the automatic specification of a policy when using a resource. If we revert our design above back to the resource approach, then we do not have to manually specify the policies. Instead, the Blueprint framework will search for policies that match this resource for each inferred action of a resource, such as getOne, getAll, and update.

// app/routers/api/rental.js

const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/rentals': {
      resource: { controller: 'rental' }
    }
  }
});

For the example above, the framework will search for the following optional policies in app/policies:

Testing your policies

You unit test your policies the same way you unit test any route. The main difference is instead of expecting a 200 response, you are expecting a 403 response. You can also check the body of the response to make sure its returning the correct failure code and failure message. Below is an example of checking the policy we created above.

// tests/unit-tests/app/routers/rental.js

const { request } = require ('@onehilltech/blueprint-testing');
const { expect } = require ('chai');

describe ('app | routers | rental', function () {
  context ('GET', function () {
    it ('should get all the rentals', async function () {
      // Send a mock request to the api, and wait for the response. This request
      // succeeds because it has the correct value for Secret-Key header.
      
      const res = await request ()
        .get ('/api/rentals')
        .set ('Secret-Key', 'ssshhh')
        .expect (200);
        
      // res.body has the text from the response. 
      // From here you can use chai to implement test oracle via assertions.
    });
    
    it ('should fail because it has invalid Secret-Key header', async function () {
      // This reqeust will fail. We are checking the response code, and
      // the text in the response.
      
      await request ()
        .get ('/api/rentals')
        .set ('Secret-Key', 'This will fail!')
        .expect (403, {
          errors: [{
            code: 'invalid_secret',
            detail: 'The request has an invalid secret.',
            status: '403'
          }]});
    });
  });
});

Last updated