Home Reference Source

Build Status Code Climate Test Coverage Issue Count Dependency Status NPM Dowloads NPM Version ESDoc

Kindergarten v1.3.5

Kindergarten is an implementation of the sandbox pattern in JavaScript with some extra goodies. Kindergarten helps you to separate your business logic into modules and add some security layer over them. Kindergarten will work well with all frameworks and libraries you like: React, Angular, Ember, Redux, Backbone and many more.

Terms Used in Kindergarten

Perimeter

Perimeter is a module that represents an area in you application (garden, kitchen, table, button, admin page etc.). Perimeter defines methods that should be exposed and rules that must be followed on that particular area.

Sandbox

Modules (perimeters) are plugged into sandbox and all exposed methods will be available there. Sandbox is governed by a governess and she makes sure that all rules are followed in order to prevent any kind of troubles.

Governess

The governess is guarding your sandbox. She makes sure that child doesn't do any unauthorized activities and she can do lot more than that! (see governesses available in Kindergarten.)

Child

Child in Kindergarten represents you current user.

Example

import {
  createPerimeter,
  createSandbox,
  guard // guard decorator
} from 'kindergarten';

import Child from './Child';
import Television from './Television';
import CableTv from './CableTv';

// Definition of the perimeter
const homePerimeter = createPerimeter({
  purpose: 'home',

  govern: {
    'can watch': [Television],
    'cannot watch': [CableTv],
    'can eat': (candy) => {
      // Only 5 candies allowed
      return this.child.eatenCandies < 5;
    },
    'cannot visitWebPage': /drugs|sex|rock-and-roll/
  },

  expose: [
    'watchTv',
    'browseInternet',
    'eat'
  ],

  watchTv(tv) {
    this.guard('watch', tv);

    console.log(`${this.childName()} is watching ${tv.type}`);
  },

  @guard
  eat(candy) {
    // decorator will call this following line automatically:
    // this.guard('eat', candy);

    console.log(`${this.childName()} is eating a candy #${++this.child.eatenCandies}`);
  },

  @guard('visitWebPage')
  browseInternet(site) {
    console.log(`${this.childName()} is browsing on following site: ${site}`);
  },

  childName() {
    return `${this.child.name}`;
  }
});

// Child
const child = new Child("John Smith Jr.");

// Definition of Sandbox
const sandbox = createSandbox(child);
sandbox.loadModule(homePerimeter);

// No problemo
sandbox.home.watchTv(new Television());

// Throws AccessDenied error
sandbox.home.watchTv(new CableTv());

// Fails after a while
for (let i = 0; i <= 6; i++) {
  sandbox.home.eat('Tasty Candy');
});

sandbox.home.browseInternet('http://google.com'); // no problem

// Throws AccessDenied error
sandbox.home.browseInternet('http://some-website-that-contains-sex.com');

Installation

npm

npm install kindergarten

Usage

Sandbox

The Sandbox is where the magic happens, that's why Kindergarten makes it really easy to create new object that acts like a sandbox. For instance you can inherit from Sandbox class:

import { Sandbox } from 'kindergarten';

class MyClass extends Sandbox {
  constructor(currentUser, perimeters) {
    this.child = currentUser
    // same as this.loadModule(...perimeters)
    this.loadPerimeter(...perimeters);
  }

  @guard
  someProtectedMethod() {
    // this method will be guarded
  }
}

You can also use @sandbox decorator, where inheritance is not applicable:

import { sandbox, createPerimeter } from 'kindergarten';
import { Component } from 'react';

const adminPerimeter = createPerimeter({
  purpose: 'admin',

  govern: {
    'can route': function (location) {
      return location === 'admin' ?
        (this.child && this.child.isAdmin) :
        true;
    }
  }
});

@sandbox(null, {
  perimeters: [
    adminPerimeter
  ]
})
@connect(
  state => ({ child: state.auth.user })
)
export default class BaseRouter extends Component {
  guardRoute(route) {
    return (nextState, replace, callback) =>
      this.guard('route', route, nextState, replace, callback);
  }

  render() {
    return (
      <Router history={history}>
        <Route path="/" component={App}>
          <IndexRoute component={HomePage} />
          <Route path="about" component={AboutPage} />
          <Route path="contact" component={ContactPage} />
          <Route path="admin" component={AdminPage} onEnter={this.guardRoute('admin')} />
          <Route path="login" component={LoginPage} />
        </Route>
      </Router>
    );
  }
}

If you don't want to use decorators, then you can use sandbox method directly:

sandbox(child, sandboxOptions)(class MyComponent extends Component {
  // ...
});

Rule

Rule class is used internally be kindergarten.

import { Rule } from 'kindergarten';
// or
// import { createRule } from 'kindergarten';
import { HeadGoverness } from 'kindergarten';

import currentUser, as child  from 'your/awesome-application/currentUser';
import Like form 'your/awesome-application/models/Like';
import Comment form 'your/awesome-application/models/Comment';

const rule1 = Rule.create('can create', [Comment]);
// or
// const rule1 = createRule('can create', [Comment]);

const rule2 = Rule.create('can create', [Like]);

const governess = new HeadGoverness();
governess.addRule(rule1, rule2); // Head Governess will learn the rules

governess.guard('create', new Like()); // no problem!
governess.isAllowed('create', Comment); // true
governess.isAllowed('create', {}); // false

Governess

Governess

The guard() method throws AccessDenied error by default, but sometimes we want something else. In react-router example above we want to redirect user to login page rather than throwing an error. We could change our guardRoute method to do that for us, but there is a better way! We can create our own governess!

import {
  HeadGoverness,
  AccessDenied
} from 'kindergarten';
export default class RoutingGoverness extends HeadGoverness {
  guard(action, route, nextState, replace, callback) {
    try {
      super.guard(action, route);
    } catch (e) {
      if (e instanceof AccessDenied) {
        replace('login');
        callback(e.message);
      } else {
        callback(e.message);
      }
    }
  callback();
  }
}

and put her on the sandbox:

// ...
import { RoutingGoverness form './governesses';

@sandbox(child, {
  perimeters,
  governess: RoutingGoverness
})
class BaseRouter extends Component {
  // ...
}

Kindergarten contains also some predefined governesses:

German Governess

German governess loves rules :trollface:. She automatically guards all exposed methods.

import {
  createPerimeter,
  createSandbox
} from 'kindergarten';

const perimeter = createPerimeter({
  purpose: 'articles',

  govern: {
    'can update': function (article) {
      return this._isAdminOrCreatorOf(article);
    }
  },

  expose: [
    'update'
  ],

  _isAdminOrCreatorOf(article) {
    return this.child.role === 'admin' || (
      this.child.role === 'moderator' &&
        this.child === article.author
    ) && !this.child.isBanned;
  },

  update(article, attrs) {
    // We don't need the line bellow GermanGoverness will add it
    // this.guard('update', article);
    return article.update(attrs);
  },
});

// ...

const currentUser = { role: 'regularGuy' };

const sandbox = createSandbox(currentUser, {
  governess: GermanGoverness
});

sandbox.loadModule(perimeter);

sandbox.articles.update(currentUser); // throws AccessDenied error

Strict Governess

Strict governess is useful if you sometimes forget to protect your exposed methods by calling guard() in their body. Strict governess throws an AccessDenied error if user calls exposed method that does not call guard() in it's body. Careful! The exposed method is executed and the AccessDenied error is thrown afterwards. Rollback is on your own! StrictGoverness might be useful during development.

import {
  StrictGoverness,
  Rule
} from 'kindergarten';

const governess = new StrictGoverness({});

governess.addRule(Rule.create('can watch', () => true);

governess.governed((tv) => {
  governess.guard('watch', tv);
  return 'hello';
}); // all right

governess.governed(() => {
  // NO guard call here
  return 'hello';
}); // throws Kindergarten.AccessDenied

Easy Governess

EasyGoverness allows child to do anything:

import { Perimeter, Sandbox, EasyGoverness } from 'kindergarten';

const perimeter = new Perimeter({
  purpose: 'playing',
  govern: {
    'cannot watch'() {
      // child can't watch anything
      return true;
    }
  },
  expose: [
    'watch'
  ],
  watch(thing) {
    this.guard('watch', thing);
  }
});

perimeter.governess = EasyGoverness;

const sandbox = new Sandbox({});
sandbox.loadModule(perimeter);
sandbox.playing.watch('bad channel'); // easy going

Middleware Governess

MiddlewareGoverness allows you to execute a given method whenever some exposed method is called through the sandbox.

import {
  createPerimeter,
  createSandbox,
  MiddlewareGoverness
} from 'kindergarten';

const child = {};

const perimeter = createPerimeter({
  purpose: 'playing',
  govern: {
    ['cannot watch']() {
      // child can't watch anything
      return true;
    }
  },
  expose: [
    'watch'
  ],
  watch(thing) {
    this.guard('watch', thing);
  }
});

const sandbox = createSandbox(child, {
  governess: new MiddlewareGoverness((governess, exposedMethod, exposedMethodCallingArgs, callingContext) => {
    // do somethig here
  });
});

sandbox.loadModule(perimeter);
sandbox.playing.watch({}); // callback above will be called

Perimeter

Perimeter contains set of rules for governess and some methods that should be exposed to sandbox.

import { Perimeter }  from 'kindergarten';

const TV = () => {};

const perimeter1 = createPerimeter({
  purpose: 'perimeter1',
  govern: {
    'can watch': [TV]
  }
});

const perimeter2 = createPerimeter({
  purpose: 'perimeter2',
  govern: {
    'cannot watch': [TV]
  }
});

const sandbox = createSandbox({});

sandbox.loadModule(perimeter1, perimeter2);

sandbox.perimeter1.isAllowed('watch', new TV()); // false
sandbox.perimeter2.isAllowed('watch', new TV()); // false
sandbox.isAllowed('watch', new TV()); // false

Notice that sandbox.perimeter1.isAllowed('watch', new TV()); is evaluated to false. It happens, because perimeter1 does not have it's own governess and therefore the governess of the sandbox is used! This is VERY VERY VERY IMPORTANT to understand! Now let's look at the same example, but this time each perimeter has it's own governess:

const TV = function () {};
const user = {};

const perimeter1 = createPerimeter({
  purpose: 'perimeter1',
  govern: {
    'can watch': [TV]
  },
  governess: new HeadGoverness(user)
});

const perimeter2 = createPerimeter({
  purpose: 'perimeter2',
  govern: {
    'cannot watch': [TV]
  },
  governess: new HeadGoverness(user)
});

const sandbox = Kindergarten.sandbox(user);

sandbox.loadModule(perimeter1, perimeter2);

sandbox.perimeter1.isAllowed('watch', new TV()); // true
sandbox.perimeter2.isAllowed('watch', new TV()); // false
sandbox.isAllowed('watch', new TV()); // false (the governess of the Sandbox is used!)

Purpose

When sandbox loads perimeter, a new Purpose is created. Sandbox copies all exposed methods into the instance of the purpose, so they are callable through the purpose namespace.

import {
  Perimeter,
  Sanbox,
  GermanGoverness,
  Purpose
} from 'kindergarten';

const perimeter = new Perimeter({
  purpose: 'foo',

  govern: {
    'cannot foo': () => true
  },

  expose: [
    'foo'
  ],

  foo: function () {}
});

const user = {};

const sandbox = new Sandbox(user, {
  perimeters: [
    perimeter
  ],

  governess: GermanGoverness
});

const myPurpose = new Purpose(perimeter.purpose, sandbox);
myPurpose._loadPerimeter(perimeter);

myPurpose.foo(); // throws Kindergarten.AccessDenied

DO NOT USE Purpose directly, it's meant to be used internally by a sandbox.

More About Kindergarten

Thanks to..

coffeeaddict for inspiration.

License

The MIT License (MIT) - See file 'LICENSE' in this project

Copyright © 2016 Jiri Chara. All Rights Reserved.