Policy Framework
Learn how to authorize access to routes
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
.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}
.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.'}
]
}
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.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. 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) }
}
}
});
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) }
}
}
});
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) }
}
}
});
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.
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) }
}
}
});
Use
all.ordered()
to evaluate the aggregates policies in order instead of in parallel.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) }
}
}
});
Use
any.ordered()
to evaluate the aggregates policies in order instead of in parallel.Last modified 1yr ago