Policy Framework

Learn how to authorize access to routes

Introduction

Policies are application entities that authorize a request. A policy is called after a request is validated and sanitized and before the request is executed on the target action. When a policy fails, the default status code is 403. Examples of policies can include

  • Verifying the value of the HTTP authorization header

  • Checking for violations of rate limits

  • Enacting a pay wall

All policies are located in app/policies.

Implementing a Policy

You implement a policy by extending the Policy class, and implementing the runCheck(req) method. The runCheck(req) method must return true if the policy passes. If the policy fails, then the runCheck(req) method must return false, or an the object {failureCode, failureMessage}.

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

module.exports = Policy.extend ({
  runCheck (req) {
    return true;
  }
});

If the policy check has an asynchronous operation, such as querying a database, then the runCheck(req) method can return a Promise. The Promise can resolve with true, false, or the object {failureCode, failureMessage}.

Default Failure Code and Message

The failureCode is an application-specific code used to identify the reason for failure. The failureMessage is a human readable message that can be displayed to the user. When apply fails, you have the option of returning the object {failureCode, failureMessage}. You also have the option of returning false. When you return false, there is specification of failureCode and failureMessage. This is where the default failureCode and failureMessage for the policy come into play.

The default failureCode and failureMessage is used when the runCheck(req) method returns false.

The default failureCode and failureMessage are just properties on the Policy. Here we have updates the passthrough policy from above with a default failureCode and failureMessage.

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

module.exports = Policy.extend ({
  failureCode: 'passthrough_failed',
  failureMessage: 'The passthrough policy failed.',
  
  runCheck (req) {
    return true;
  }
});

Now, when the runCheck(req) method returns false, it will send the following response:

{
  errors: [
    {status: '403', code: 'passthrough_failed', detail: 'The passthrough policy failed.'}
  ]
}

Setting Parameters

Policies can also take parameters, which can be used to configure dynamic behavior in a policy. For example, what if we want the passthrough policy to be configured with the result of true or false. This means we need a way to configure the policy with the expected result. We do this by via policy parameters.

To support parameters, implement the setParameters() method on the policy. Each argument in setParameters() is an individual parameter. Below, we have updated our simple passthrough policy to use a parameter to define the result of the policy.

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

module.exports = Policy.extend ({
  failureCode: 'passthrough_failed',
  failureMessage: 'The passthrough policy failed.',
  
  value: true,    // default value is true
  
  setParameters (value) {
    this.value = value;
  },
  
  runCheck (req) {
    return this.value;
  }
});

As illustrated above, the setParameters(value) method stores the parameter value. Likewise, the runCheck(req) method uses the parameter value as its result.

Applying Policies to Routes

Now that we have defined our passthrough policy, our next step is to apply the policy to different routes. We apply a policy to a route by naming the policy using the policy property in the router specification.

Use dot notation to access policies located in subdirectories. For example, the policy a/b/c can accessed using the name a.b.c.

In this example, we are applying the passthrough polices to all routes under the /messages path.

app/routers/message.js
const { Router } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      policy: 'passthrough',
      post: { action: 'message@create' }
    }
  }
});

Now, anytime we the client sends a request to /messages on the application server, the passthrough policy will authorize the request.

Passing Parameters

The default behavior of the passthrough policy is allow the authorization to succeed. This is because the default value of the value property is true. But, what if we want the authorization to fail. The passthrough policy supports parameters, but we need to pass false to as a parameter value to the policy.

If you need to pass parameters to a policy, then you must use the check(name, ...args) method. The first parameter to the check() method is the name of the policy. The remaining arguments are the parameters to the policy in-order of their argument specification in setParameters().

We have now updated the example so that the create route will experience a policy failure.

app/routers/message.js
const { Router, policies: { check } } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      policy: 'passthrough',
      post: { action: 'message@create', policy: check ('passthrough', false) }
    }
  }
});

Optional Policies

An optional policy is a policy that is applied if it exists. This is useful when you are defining a router in a Blueprint module, and want to give the module user the option of applying a policy to a route. To declare a policy on a route optional, begin the name with a question mark (?). For example, the create action now has an optional policy.

app/routers/message.js
const { Router, policies: { check } } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      policy: 'passthrough',
      post: { action: 'message@create', policy: check ('?passthrough', false) }
    }
  }
});

Negating Policies

Similar to optional policies, you can also negate a policy. For example, if the policy returns true, then the negated policy will return false. If the negated policy returns false or {failureCode, failureMessage}, then it will return true. To negate a policy, begin the policy name with a exclamation point (!). For example, the create action now has a negated policy.

app/routers/message.js
const { Router, policies: { check } } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      policy: 'passthrough',
      post: { action: 'message@create', policy: check ('!passthrough', false) }
    }
  }
});

Aggregate Policies

An aggregate policy is a policy created by combining one or more policies. There are two common use cases supported by default. Either all the policies succeed or any of the policies succeed for the aggregate policy to succeed.

all

Use the all() method to create an aggregate policy where all policies must succeed in order for the aggregate policy to succeed. The all() method takes a list of policies, and an optional failureCode and failureMessage parameter.

app/routers/message.js
const { Router, policies: { check, all } } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      policy: all ([
        'passthrough',
        check ('passthrough', true),
        all (['passthrough', check ('!passthrough', false)])
      ], 'passthroughs_failed', 'All passthrough policies failed')    
      post: { action: 'message@create', policy: check ('!passthrough', false) }
    }
  }
});

Ordered execution

Use all.ordered() to evaluate the aggregates policies in order instead of in parallel.

any

Use the any() method to create an aggregate policy where at least one policies must succeed in order for the aggregate policy to succeed. The any() method takes a list of policies, and an optional failureCode and failureMessage parameter.

app/routers/message.js
const { Router, policies: { check, all } } = require ('@onehilltech/blueprint');

module.exports = Router.extend ({
  specification: {
    '/messages': {
      policy: any ([
        'passthrough',
        check ('passthrough', true),
        any (['passthrough', check ('!passthrough', false)])
      ], 'passthroughs_failed', 'All passthrough policies failed')    
      post: { action: 'message@create', policy: check ('!passthrough', false) }
    }
  }
});

Ordered execution

Use any.ordered() to evaluate the aggregates policies in order instead of in parallel.

Last updated