Security News
Oracle Drags Its Feet in the JavaScript Trademark Dispute
Oracle seeks to dismiss fraud claims in the JavaScript trademark dispute, delaying the case and avoiding questions about its right to the name.
feathers-rules
Advanced tools
A FeathersJS plugin for rule based authorization. The concept is loosely based on Firebase Firestore Rules.
The plugin forbids every request by default unless there is a rule allowing access. Rules are plain JavaScript functions that ether return true or false. If a rule returns true the access is granted and no other rule returning false can revoke the access. If no rule returns true a Forbidden-Exception is thrown. So feathers-rules follows an "allow access if any of the rules is followed" approach.
In your app file import the protectServices()
function:
const { protectServices } = require('feathers-rules')
Add the following code after you defined all your hooks:
app.configure(protectServices())
If you created your app with the default feathers generator, a good place is after the service & channel configuration:
app.configure(services)
app.configure(channels)
app.configure(protectServices())
A simple rules
object could look like this:
export const blogPostRules = {
find: (context) => {
// Only allow querying for the users documents
return context.params.query && context.params.query.userId === context.params.user._id
},
create: (context) => {
// Only allow creating a document if user creates a document for him-/herself
return context.data && context.data.userId === context.params.user._id
},
}
The method names are equal to the service methods (find | get | create | update | patch | remove
) and the values are functions that get feathers' HookContext
as input and return a boolean
or a Promise
of a boolean
.
To grant access to your services you add the allow(rules)
hook to your app / service hooks:
blogPostService.hooks({
before: {
all: [
allow(blogPostRules),
],
},
})
You can use the following special keywords as method names, too:
all
: Is checked for every methodread
: Is checked for find
& get
write
: Is checked for create
, update
, patch
& remove
For example:
export const blogPostRules = {
read: (context) => {
// Allow everyone to get or find all posts
return true
}
}
Another way to run one rule for multiple service methods is to use single letter rules:
export const rules = {
// Runs only for create, update & patch
cup: (context) => {
return context.params.query && context.params.query.userId === context.params.user
}
}
Single letter rules support the following mapping:
c --> create
f --> find
g --> get
p --> patch
r --> remove
u --> update
You can combine them as you like. cup
& upc
both work.
As described above a Forbidden-Exception is thrown.
The exceptions message gives insights what went wrong. But because all rules have returned false there is no single rule that forbid access. Hence we can't say something like "Your query does not contain your userId". (To get such behavior see Throwing RulesError in Rules)
Therefore the exception message contains the service name, method, id, data & query of the request. This helps when you are sending many requests and want to know which one failed (especially helpful when debugging websockets).
To add an additional layer of protection the data properties password
, newPassword
& oldPassword
are replaced with a placeholder.
You can add additional protectedFields
. See Advanced configuration.
If you want to show errors to the user (like validation errors) you can throw a RulesError
with an array of ErrorInfos
.
// Using some validation library
const { isValid, errors } = validate(context.data, schema)
if (!isValid) {
throw new RulesError(errors)
}
A RulesError
will be translated to a BadRequest
only if all rules returned false
(to be more specific: not true
).
If some rule returned true
the access is always granted.
Here is an example:
// admin.rules.js
export const rules = {
write: (context) => {
return context.params.user.role === 'admin'
}
}
// user.rules.js
const schema = require('./user.schema.json')
export const rules = {
write: (context) => {
const { isValid, errors } = validate(context.data, schema)
if (!isValid) throw new RulesError(errors)
}
}
In this example the admin can do whatever she wants but user requests will always be validated.
If the admin is sending a request the RulesError
will be thrown because all rules are always run.
Nevertheless, the request will succeed because the admin rule returns true.
No error is shown to the admin.
If the user is sending the request a BadRequest
is thrown with the errors from RulesError
.
This is due to the fact that the admin rule returns false
and the user rule throws --> no rule returns true.
The request is not allowed and therefore the errors are send to the client.
IMPORTANT: Currently only the errors from the first throwing rule are send to the client.
You should not run the users query in your find
or get
rule and check if the user has access to the returned documents.
This can lead to side channel attacks like measuring the time how long the request took and figuring out if and approx. how many documents the rule has checked.
Always check the query directly: Does the query contain the userId, etc.?
If you have many allow(rules)
calls in your hooks, try to escape as early as possible within your rules.
All rules are executed each time a request is sent.
If you have heavyweight rules (e.g. querying the database), try putting them at the end of your function:
export const rules = {
write: async (context) => {
// Lightweight rules block
const { isValid, errors } = validate(context.data, lightweightRules)
if (!isValid) throw new RulesError(errors) // Early escape
// Heavyweight rules block
const allTheData = await context.app.service('someService').find({})
// Do some complex validation here...
if (passedComplexValidation) return true
return false
}
}
The protectServices()
function is preconfigured with the following options.
You can overwrite them.
const options = {
// Services that should not be protected
omitServices: ['authentication'],
// function that checks if params.allow is true, else throws exception
allowedChecker: allowedChecker(),
}
app.configure(protectServices(options))
The allowChecker can be configured with the following options:
{
// The text that is printed instead of the fields value
protectWord: '[HIDDEN]',
// Fields in context.data that should be replaced with protectWord in the exception message
protectedFields: ['password', 'newPassword', 'oldPassword'],
}
To add those options you must configure protectServices()
with your own allowChecker:
app.configure(protectServices({
allowedChecker: allowedChecker({
protectWord: 'NOTHING-TO-SEE-HERE'
})
}))
I need to disallow access in a certain rule. Returning false does not help because another rule keeps granting access. What can I do?
You can always throw an Forbidden-Exception in your rules. This breaks the whole hook chain in feathers and denies the access.
FAQs
Use firestore like rules in feathers to secure your services
The npm package feathers-rules receives a total of 0 weekly downloads. As such, feathers-rules popularity was classified as not popular.
We found that feathers-rules demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Oracle seeks to dismiss fraud claims in the JavaScript trademark dispute, delaying the case and avoiding questions about its right to the name.
Security News
The Linux Foundation is warning open source developers that compliance with global sanctions is mandatory, highlighting legal risks and restrictions on contributions.
Security News
Maven Central now validates Sigstore signatures, making it easier for developers to verify the provenance of Java packages.