Serrurier, a declarative extension for methods access control in jagi:astronomy(v2) using decorators
ℹ Serrurier and cadenas are french words that stands respectively for locksmith and padlock.
✔ This library aims to write more secure, maintainable and readable code, by defining function access through decorators called@cadenas
.
✔ It integrates smoothly with alanning:roles.
✔ Helps to abstract error management (user feedbacks, security reports) through the flexible reporter API.
✔ Allows to define Astro methods that remote-call on server.
✔ Works with synchronous and asynchronous (through Meteor methods api) methods.
✔ Compatible with ValidationErrors.
⚠ To use es7 feature candidate decorators in your meteor project (@
), you need to follow those 2 straightforward steps.
meteor add svein:serrurier
- 🔒 @cadenas decorator
- @server decorator
- Adding legacy decorations (Meteor >= 1.3.4)
- Security in production
- Reporters
- Write your own @cadenas
- Run tests
ℹ A
@cadenas
is an assertion that will trigger a specificException
when it fails.
ℹ This (or those) assertions are run both client side and server side.
ℹ Those Exceptions can be handled by reporters.
ℹ The general syntax for@cadenas
is@cadenas( cadenasName, ...params )
ℹ@cadenas
can target any function inside amethods
description block.
ℹ@cadenas
can target anyevents
handlers but not in an array of handlers. On anyError
thrown by a cadenas,e.preventDefault()
will be called.
ℹ It supports callbacks formethods
.
ℹ Serrurier is a very modular library and you can easely write your own@cadenas
within few lines of codes.
⚠ To useloggedUserInRole
@cadenas
for alanning:roles, you need to addsvein:serrurier-cadenas-roles
to your project.meteor add svein:serrurier-cadenas-roles
import { Serrurier, cadenas } from 'meteor/svein:serrurier';
import { Mongo } from 'meteor/mongo';
const Project = Serrurier.createClass({
name: 'Project',
collection: new Mongo.Collection( 'projects' ),
methods: {
// This is it
@cadenas( 'loggedUserInRole', 'administrator' )
updateSensitiveData() {
// ...
console.info( 'The assertion passed, user is administrator.' );
}
}
});
Then, if logged user is not in role 'administrator' and calls
(new Project()).updateSensitiveData();
This will output in the console ( if Serrurier.silence()
has not been called ) :
Notice that the cadenas 'userIsLoggedIn'
has passed, because 'loggedUserInRole'
cadenas depends on it.
The order you declare your cadenas is the order they will be applied.
@persisted()
@userIsLoggedIn()
aMethod() {
// ...
}
Serrurier will first check if the astro instance has been persisted, then it will check if user is logged in.
### Default *`@cadenas`*ℹ If you want, and you should, write your own cadenas, go to this section.
⚠ To have those cadenas available in your app, you will have to add them manuallymeteor add svein:serrurier-cadenas-defaults
package
svein:serrurier-cadenas-defaults
asserts that the user is logged in, withMeteor.userId
.
targetsmethods
,events
throwsSecurityException
params none
package
svein:serrurier-cadenas-defaults
asserts that all method arguments match the given paramsDescription.
targetsmethods
throwsValidationException
paramsparamsDescription An array of Meteor Match Patterns
package
svein:serrurier-cadenas-defaults
asserts that the first argument of the class instance method is a string corresponding to an existing user.
targetsmethods
throwsStateException
params none
package
svein:serrurier-cadenas-defaults
asserts that the instance it is being called upon has been persisted (with_isNew
property to false)
targetsmethods
,events
throwsStateException
params none
⚠ This cadenas depends on
svein:serrurier-cadenas-defaults
, so it will be automatically imported if missing.
⚠ You need to use alanning:roles (v1 and v2 are supported) in your project to use this@cadenas
, and add the following package :meteor add svein:serrurier-cadenas-roles
package
svein:serrurier-cadenas-roles
asserts that the logged user has role(s) in a specific scope (partition).
targetsmethods
,events
throwsSecurityException
depends on'userIsLoggedIn'
(will always check that user is logged in first)
paramsrole_s One single or an array of role(s), i.e. string(s).
partition The scopes in which the partition will apply. There is one special partition AUTO that resolves tothis.getPartition()
in the astro class instance, seee below.import { parts } from 'meteor/svein:serrurier-cadenas-roles';
// ... @cadenas( 'loggedUserInRole', 'responsible', parts.AUTO ) // ... // parts.GLOBAL is the default value, it maps straight to Roles.GLOBAL_PARTITION for roles v2, // Roles.GLOBAL_GROUP for roles v1.X @cadenas( 'loggedUserInRole', 'responsible', parts.GLOBAL )
meteor add svein:serrurier-decorator-server
ℹ Applies to
methods
only.
ℹ Performs server-side only, you must provide a callback as last argument if you need the return value.
ℹ This callback has the following signature :callback( [ Exception ] exception, { * } result )
⚠ You must always open and close parenthesis when using decorators
// @locus client and server
import { Serrurier, server } from 'meteor/svein:serrurier';
//... inside a Serrurier.createClass `methods` field
@server()
aMethodThatMustExecuteOnServer() {
console.info( "Look, I'm running on server only." );
}
Calling aMethodThatMustExecuteOnServer
from client will call it on server. In the background, a Meteor method will be registered with the name /serrurier/ClassName#methodName
through Meteor.methods
.
import { Serrurier, server } from 'meteor/svein:serrurier';
import { ValidationError } from 'meteor/mdg:validation-error';
//... inside a Serrurier.createClass `methods` field
MyClass = Serruroer.createClass({
name: 'MyClass'
methods: {
@server()
aMethodThatThrowsValidationError() {
throw new ValidationError([
{
name: 'cost',
type: 'out-of-range',
value: 162,
min: 0,
max: 100
}
]);
}
}
});
// ... later
/** @type MyClass */
myClass.aMethodThatThrowsValidationError( function( err ) {
console.info( ValidationError.is(err) );
// prints 'true'
});
Follow those two simple steps :
meteor npm install -s babel-plugin-transform-decorators-legacy
Then add at the root of your project a .babelrc
file with the following content :
{
"plugins": [
"transform-decorators-legacy"
]
}
That's all you have to do!
You can prevent Serrurier
from outputting anything in the console, and lock the API with one single Serrurier.lock()
at the beginning of your application.
This cannot be reversed. Any consequitive call to any Serrurier
static method will be ignored.
import { Meteor } from 'meteor/meteor';
import Serrurier from 'meteor/svein:serrurier';
if(Meteor.isProduction) Serrurier.lock();
ℹ A reporter is exactly like an event listener for errors.
ℹ For each type of error, i.e.SecurityException
,StateException
,ValidationException
(or custom exceptions), you can register a reporter.
ℹ You can create your own exceptions withSerrurier.createException
.
ℹ By default, there is no reporting : the errors are just thrown up to the method call.
ℹ A reporter takes onesecurity_context
argument that holds several informations :
* @typedef {object} security_context
* An object that holds information about the context of the execution.
*
* @prop {!string} action - The 'Class#method' Astronomy signature who built the context
* @prop {!string} reason - Why the access was forbidden? Recommanded format
* is a short dot separated description, with arguments separated by columns.
* this.is.an.example:arg1:arg2
* @prop {!string} exceptionId - Unique identifier of the exception
* @prop {!string} stackTrace - The stacktrace that generated this exception
* @prop {!object} target - The target of the action
* @prop {object=} currentTarget - The currentTarget of the action, i.e. a
* nested field of the target
*/
This reporter is qualified 'isolated' because it listen to exceptions thrown on the environment it is defined (client and/or server).
// @locus client and/or server
import { Serrurier, SecurityException } from 'meteor/svein:serrurier';
// if you need a client-only or server-only reporter, just call this code from one or the other.
Serrurier.registerIsolatedReporter( SecurityException, function( context ) {
console.info( 'hi!' );
// prints 'hi!' in server if the exception is thrown server side,
// in client if the exception is thrown client side.
});
If you need a reporter that is executed on server, but listens to both client and server side exceptions, you need to use those functions :
Server side :
// @locus server
import { SecurityException, Serrurier } from 'meteor/svein:serrurier';
Serrurier.publishServerReporter( SecurityException, function ( context ) {
console.warn( 'hi!' );
// prints 'hi!' in server, whether the exception was thrown client or server side.
});
Client side :
// @locus client
import { SecurityException, Serrurier } from 'meteor/svein:serrurier';
Serrurier.subscribeServerReporter( SecurityException );
Suits nicely for Error logging and suspect activity logging, see the Paranoid reporter bellow.
This reporter listen for SecurityException
s on both client and server, and log detailed information in the server console.
It also keep track of those reports for 2 months.
meteor add svein:serrurier-reporter-paranoid
This is how a report looks like in the server console.
_______________________________ SERRURIER PARANOID REPORT _______________________________
createdAt: new Date('2016-07-07T05:46:25.005Z'),
ip: '127.0.0.1',
geoInfo: 'localhost'
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) ...,
securityContext: {
reason: 'user.not.in.role:administrator:GLOBAL',
exceptionId: 'loggedUserInRole',
action: 'Project#updateSensitiveData',
stackTrace: '...',
target: {
Project: {
plugins: {
task: {
_types: []
},
annotation: {
_types: []
}
},
isOpen: false,
publicationPolicy: true,
enablePlugins: false,
defaultCaptionSource: null,
pending: []
}
},
userId: 'JCwWgQZLExz5KrcDH'
}
_________________________________________________________________________________________
You must import the package on server to config
it :
// @locus server
import {
config,
ONE_DAY,
ONE_MONTH,
ParanoidReports // This is the mongo collection
} from 'meteor/svein:serrurier-reporter-paranoid';
config({
// [default : false]
geotracking: true,
// [default : one day] time in seconds the ip records will be kept
ip_cache_ttl: ONE_DAY*2,
// [default : two months] time in seconds the security records will be kept
record_ttl: ONE_MONTH
});
// @locus client and server
import { Cadenas } from 'meteor/svein:serrurier';
import { Match } from 'meteor/check';
/**
* Assert the logged user is administrator
* Note that you can override the ExceptionClass property when making partials.
*/
const loggedUserIsAdmin = Cadenas.partialFrom( 'loggedUserInRole' , {
name: 'loggedUserIsAdmin'
}, 'administrator' );
Later in your project
@cadenas( 'loggedUserIsAdmin' )
methodThatMustBeRunByAdmin() {
// Will be run by 'administrator'
}
// @locus client, server
import { DefaultCadenas, Serrurier } from 'meteor/svein:serrurier';
// You can also use builtin exception like ValidationException, SecurityException and StateException
const MyException = Serrurier.createException( 'MyException' );
const myCustomCadenas = new DefaultCadenas({
name: 'myCustomCadenas',
// [optional] The exception that will be thrown. Only reporters listening for this exception will be
// called upon assertion failures.
// Default to SecurityException for 'DefaultCadenas' and ValidationException for 'MethodParamsCadenas'
// You shall use the utility function `Serrurier.createException` if you need to create your own.
// They inherit Meteor.Error and can be thrown from server to client via callabcks.
ExceptionClass: MyException
doesAssertionFails: function( myArg ) {
// Does it need to throw an exception ?
// Must NOT throw an error. Returns a non-empty string that will result in the
// `reason` field for context when the assertion fails, a falsy value otherwise.
},
// The cadenas signature (i.e. `doesAssertionFails` signature)?
// You must describe any parameter here to keep the API consistent.
// Use Match.Any if you don't want to test an input, however this is not recommanded.
matchPatterns: [ Match.Optional( String ) ],
// [optional] A set of depending assertions in the form of a dictionary which keys are cadenas names,
// and values an array with their `doesAssertionFails` params.
dependingCadenas: { userIsLoggedIn: [] }
});
Inside the packages/serrurier folder :
meteor npm test