UNPKG

spel2js

Version:

Parse Spring Expression Language in JavaScript

309 lines (227 loc) 9.44 kB
# spel2js [![Build Status][build-image]][build-url] [![Test Coverage][coverage-image]][coverage-url] [![Dependency Status][depstat-image]][depstat-url] [![Bower Version][bower-image]][bower-url] [![NPM version][npm-image]][npm-url] <!--[![Code GPA][gpa-image]][gpa-url] [![IRC Channel][irc-image]][irc-url] [![Gitter][gitter-image]][gitter-url] [![GitTip][tip-image]][tip-url]--> ## About SpEL2JS is a plugin that will parse [Spring Expression Language](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html) within a defined context in JavaScript. This is useful in single-page applications where duplication of authorization expressions for UI purposes can lead to inconsistencies. This library implements a JavaScript version of the parser based on the documentation in the link above. I did my best to followed the docs as closely as possible, but if you come accross an expression that behaves differently than you would expect then please open an issue. ## Getting Started Install SpEL2JS: ```sh $ npm i -S spel2js # or $ bower i -S spel2js ``` Or [download the zip](https://github.com/benmarch/spel2js/archive/master.zip) Include the dependency using a module loader or script tag. ## Usage SpEL2JS exports a singleton with two members: ```js import spel2js from 'spel2js'; console.log(spel2js); /* { StandardContext, SpelExpressionEvaluator } */ ``` ### `StandardContext` The `StandardContext` is a factory that creates a evaluation context for an expression. **NOTE:** This is not the same as the Java `EvaluationContext` class, though it serves a similar purpose. ```js let spelContext = spel2js.StandardContext.create(authentication, principal); ``` The `create()` method takes two arguments: `authentication` and `principal` `authentication` is an instance of Spring's [`Authentication`](https://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/core/Authentication.html) class from Spring Security. `principal` is any object representing the user (this is just used for reference, and can be any value or structure) ### `SpelExpressionEvaluator` The heavy lifting is done using the `SpelExpressionEvaluator` which exposes two functions: `compile()` and `eval()` `compile()` pre-compiles a SpEL expression, and returns an object with an `eval()` method that takes a context and optional locals: ```js import { StandardContext, SpelExpressionEvaluator } from 'spel2js'; const expression = '#toDoList.owner == authentication.details.name'; const spelContext = StandardContext.create(authentication, principal); const locals = { toDoList: { owner: 'Darth Vader' } }; const compiledExpression = SpelExpressionEvaluator.compile(expression); compiledExpression.eval(spelContext, locals); // true ``` `eval()` is just a shortcut for immediately evaluating an expression instead of pre-compiling: ```js import { StandardContext, SpelExpressionEvaluator } from 'spel2js'; const expression = '#toDoList.owner == authentication.details.name'; const spelContext = StandardContext.create(authentication, principal); const locals = { toDoList: { owner: 'Darth Vader' } }; SpelExpressionEvaluator.eval(expression, spelContext, locals); // true ``` ### Recommended Usage Create a single context that contains information about the current user and reuse it for all evaluations. This way, you only have to supply an expression and locals when evaluating. Always pre-compile your expressions! Compilation takes much longer than evaluation; doing it up-front saves CPU when evaluating later. ## Example Say you are creating a shared to-do list, and you want to allow only the owner of the list to make changes, but anyone can view: ```java //ListController.java @Controller @RequestMapping('/todolists') public class ListController { public static final String ADD_LIST_ITEM_PERMISSION = "#toDoList.owner == authentication.details.name"; ... @PreAuthorize(ADD_LIST_ITEM_PERMISSION) @RequestMapping(value="/{toDolistId}/items", method=RequestMethod.POST) public ResponseEntity<ListItem> addListItem(@MagicAnnotation ToDoList toDoList, @RequestBody ListItem newListItem) { //add the item to the list return new ResponseEntity<ListItem>(newListItem, HttpStatus.CREATED); } ... } ``` ```js //spel-service.js import { StandardContext, SpelExpressionEvaluator } from 'spel2js'; // wraps spel2js in a stateful service that simplifies evaluation angular.module('ToDo').factory('SpelService', function () { return { context: null, // assume this is called on page load setContext(authentication, principal) { this.context = StandardContext.create(authentication, principal); }, getContext() { return this.context; }, compile(expression) { const compiledExpression = SpelExpressionEvaluator.compile(expression); return { eval(locals) { return compiledExpression.eval(this.getContext(), locals); } }; }, eval(expression, locals) { return SpelExpressionEvaluator.eval(expression, this.getContext(), locals); } }; }); //list-controller.js angular.module('ToDo').controller('ListController', ['$http', '$scope', 'SpelService', function ($http, $scope, SpelService) { // retrieve all permissions and pre-compile them $http.get('/api/permissions').success(function (permissions) { angular.forEach(permissions, function (spelExpression, key) { $scope.permissions[key] = SpelService.compile(spelExpression); }); }); // $scope will be used as locals $scope.list = { name: 'My List', owner: 'Ben March', items: [ { text: 'List item number 1!' } ] } // EXPAMPLE 1: authorize a request before making it $scope.addListItem = function (list, newListItem) { if ($scope.permissions.ADD_LIST_ITEM_PERMISSION.eval($scope)) { $http.post('/todolists/' + list.id + '/items', item).success(function () {...}); } } }]); ``` ```html <!--list-controller.html--> <div ng-controller="ListController"> ... <li ng-repeat="listItem in list.items"> <p>{{listItem.text}}</p> </li> <li class="list-actions"> <input type="text" ng-model="newListItem.text" /> <!-- EXAMPLE 2: Hide the button if the user does not have permission --> <button ng-click="addListItem(list, newListItem)" ng-if="permissions.ADD_LIST_ITEM_PERMISSION.eval(this)">Add</button> </li> ... </div> ``` Now the UI can always stay in sync with the server-side authorities. ## Features This is now in a stable state and will be released as 0.2.0. The following features are tested and working: - Primitive Literals - Property references - Compound expressions - Comparisons - Method references - Local variable reference ("#someVar") - Math - Ternary operators - Safe navigation - Assignment - Complex literals - Projection/selection - Increment/Decrement - Logical operators (and, or, not) - hasRole() (if you use spel2js.StandardContext) The following are not implemented yet because I'm not sure of the best approach: - Qualified identifiers/Type references/Bean References - hasPermission() for custom permission evaluators If someone wants to implement a REST-compliant way in Spring to expose the permissions (and maybe the custom PermissionEvaluators) that would be awesome. ## Building Locally ```sh $ npm i $ npm run build $ npm test ``` ## Credits Credit is given to all of the original authors of the Java SpEL implementation at the time of this library's creation: - Andy Clement - Juergen Hoeller - Giovanni Dall'Oglio Risso - Sam Brannen - Mark Fisher - Oliver Becker - Clark Duplichien - Phillip Webb - Stephane Nicoll - Ivo Smid This repository was scaffolded with [generator-microjs](https://github.com/daniellmb/generator-microjs). ## License Since this was ported from the Spring Framework, this library is under version 2.0 of the [Apache License](http://www.apache.org/licenses/LICENSE-2.0). [build-url]: https://travis-ci.org/benmarch/spel2js [build-image]: http://img.shields.io/travis/benmarch/spel2js.png [gpa-url]: https://codeclimate.com/github/benmarch/spel2js [gpa-image]: https://codeclimate.com/github/benmarch/spel2js.png [coverage-url]: https://codeclimate.com/github/benmarch/spel2js/code?sort=covered_percent&sort_direction=desc [coverage-image]: https://codeclimate.com/github/benmarch/spel2js/coverage.png [depstat-url]: https://david-dm.org/benmarch/spel2js [depstat-image]: https://david-dm.org/benmarch/spel2js.png?theme=shields.io [issues-url]: https://github.com/benmarch/spel2js/issues [issues-image]: http://img.shields.io/github/issues/benmarch/spel2js.png [bower-url]: http://bower.io/search/?q=spel2js [bower-image]: https://badge.fury.io/bo/spel2js.png [downloads-url]: https://www.npmjs.org/package/spel2js [downloads-image]: http://img.shields.io/npm/dm/spel2js.png [npm-url]: https://www.npmjs.org/package/spel2js [npm-image]: https://badge.fury.io/js/spel2js.png [irc-url]: http://webchat.freenode.net/?channels=spel2js [irc-image]: http://img.shields.io/badge/irc-%23spel2js-brightgreen.png [gitter-url]: https://gitter.im/benmarch/spel2js [gitter-image]: http://img.shields.io/badge/gitter-benmarch/spel2js-brightgreen.png [tip-url]: https://www.gittip.com/benmarch [tip-image]: http://img.shields.io/gittip/benmarch.png