Skip to content

Commit

Permalink
Added new access handling with the ability determine model based on g…
Browse files Browse the repository at this point in the history
…roup membership
  • Loading branch information
kriszyp committed Sep 29, 2014
1 parent 5bb6e5a commit ae765b4
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 48 deletions.
148 changes: 105 additions & 43 deletions README.md
Expand Up @@ -71,15 +71,14 @@ of exposing that model through an HTTP/REST API. A simple example of a model is:
}
});

We can then expose this data model through Pintura's HTTP REST interface by implementing
the getDataModel function on the pintura module. This function is called for each HTTP
request:
We can then expose this data model through Pintura's HTTP REST by registering our
models through the `pintura/jsgi/access` module. The module description contains
more information on registering modules for different groups and users,
but we can easily just register our model for use for everyone:

require("pintura/pintura").getDataModel = function(request){
return {
Product: Product
};
};
require("pintura/jsgi/access").registerModels({
Product: Product
});

Our data model will then be available at the path of /Product/ such that we can make
HTTP requests like GET /Product/2.
Expand Down Expand Up @@ -132,7 +131,8 @@ is [described in this article](http://www.sitepen.com/blog/2010/03/08/object-cap
Facets are used to define the different levels of access for models. Pintura's security
configuration object can then be configured to define how users are authenticated
and which facets or access levels each user is given. The security configuration object
is available at require("pintura/pintura").config.security. The primary functions
is available at require("pintura/pintura").config.security, and can be configured
by calling the `configure` method with object properties to be assigned. The primary functions
that can be overriden or used are:

* authenticate(username, password) - The authenticate method
Expand All @@ -150,47 +150,92 @@ and facet for access to the user model (for unauthenticated users).
For example, we could choose to store passwords in plaintext by changing the
encryptPassword method to a return the password unchanged:

require("pintura/pintura").config.security.encryptPassword = function(username, password){
return password;
};
require("pintura/pintura").configure({
security: {
encryptPassword: function(username, password){
return password;
}
}
});

## Access After Authentication

Once authentication is established, we could then use the user's authentication state to restrict or allow access to different
parts of the application data model. For example, we could check to see if a user is
logged to determine if we should provide access to the "Secret" data:
Once authentication is established, access to the data is determined by group membership.
We can define group membership on the security object as well. The security object
has several methods that are used to compute the groups for a user, and which models
to expose based on these groups.

The simplest way to define access to models is to define the groups for users on the
security object's `groupsUsers`. We can do this be assigning groups to `groupsUsers`
object by properties with the group name and a value of an array of the included users:

var pintura = require("pintura/pintura");
pintura.configure({
security: {
groupsUsers: {
// define john to be the only admin
admin: ['john'],
// define all users to be in the common group
common: ['*'],
// define unauthenticated users to be in the public group
public: [null]
}
}
});

var publicModel = {
Product: Product
};
var authorizedModel = {
Product: Product,
Secret: SecretModel
};
require("pintura/pintura").getDataModel = function(request){
var user = request.remoteUser;
if(user){
return authorizedModel;
We could then register our data models with group information associated with it. We
use the registerModels function to accomplish this. This can defined with an object where
properties define the model by name, where each property value can be a model, a model
with the groups allowed, or an array of models with groups. This is best illustrated
by an example:

pintura.registerModels({
// The User model is exposed (though /User/), regardless of which user/group
User: User,
// Expose the Product model, dependent on the group
Product: [
{
// The main (unrestricted) Product model, for anyone in the admin group
model: Product,
groups: ['admin']
},
{
// The public (restricted) Product facet, for those in the user or public group
model: PublicProduct,
groups: ['user', 'public']
}
],
}, {
// define a set of models to be exposed for the admin groups
groups: ['admin'],
models: {
// we could define multiple models (that are available to the admin),
// but here we are just defining the File model to be exposed.
File: File
}
return publicModel;
};
});


We could also potentially have a data model that is readonly for some users and
editable for others. In the example above, we could specify that the Product table
is readonly for users that are not logged in:
editable for others. In the example above, we had specified Product table
to be readonly for users that do not have admin access, we can create this read-only
facet using the Restrictive facet constructor:

var Restrictive = require("perstore/facet").Restrictive;
var publicModel = {
// the Product table is restricted to readonly for public access
Product: Restrictive(Product)
};
var authorizedModel = {
// the Product table is unrestricted here for authorized users
Product: Product,
Secret: SecretModel
};
// assign the data model based on authentication as above

var PublicProduct = Restrictive(Product);

The security object also includes the following methods and properties that can be overriden or used to
provide customized determination of group membership or access to data models:

* groupsUsers - This is the object that defines the users in each group, as described above.
* getGroupsForUser(user) - This should return an array of groups, for which the user
has membership.
* getModelForUser(user) - This should return the specific set of data models for the given user.
This can be overriden to define a custom method for determining the data model.

In addition, you can also alternately define a `getDataModel(request)` method on the pintura
module object, to determine the data model.

Error Handling
===========

Expand Down Expand Up @@ -508,9 +553,23 @@ The return app will then have standard array methods available for modifying the

The `indexOf` and `lastIndexOf` methods also support a string id as the argument, in which case it will search for a config with that id.

## access

{
module: 'pintura/jsgi/access',
config: security
}

The access module provides access to the data models, based on the group membership of the currently
authenticated user. The group membership is typically defined on the security object, which is
then used by access module to calculate the data models available for that user.

## auth

app = require('pintura/jsgi/auth')(security, nextApp);
{
module: 'pintura/jsgi/auth',
config: security
}

The auth module handles HTTP authorization, performing the HTTP request side of user
authentication and calling the security module to perform the authentication and determine the authorization of
Expand All @@ -521,7 +580,10 @@ next app as the second argument.

## rest-store

app = require('pintura/jsgi/rest-store')(config);
{
module: 'pintura/jsgi/rest-store',
config: config
}

This module delegates the HTTP REST requests to the appropriate data model. This
component will call the method on the model corresponding the request method name
Expand Down
10 changes: 10 additions & 0 deletions jsgi/access.js
@@ -0,0 +1,10 @@
var AccessError = require("perstore/errors").AccessError;

exports = module.exports = function(security, nextApp){
return function(request){
var user = request.remoteUser || null;
// define the dataModel for the request
request.dataModel = security.getModelForUser(user);
return nextApp(request);
};
};
1 change: 0 additions & 1 deletion jsgi/rest-store.js
Expand Up @@ -12,7 +12,6 @@ var METHOD_HAS_BODY = require("./methods").METHOD_HAS_BODY,

module.exports = function(options){
return function(request){
// N.B. in async, options.getDataModel() can be a promise, so have to wait for it
return when(options.getDataModel(request), function(model){
var path = request.pathInfo.substring(1);
var scriptName = request.scriptName;
Expand Down
21 changes: 18 additions & 3 deletions pintura.js
Expand Up @@ -15,20 +15,33 @@ require('./media/plain');
require('./media/message/json');

var configure = require('./jsgi/configure');
var deepCopy = require('perstore/util/copy').deepCopy;
var config = exports.config = {
mediaSelector: require('./media').Media.optimumMedia,
database: require('perstore/stores'),
security: require('./security').DefaultSecurity(),
responseCache: require('perstore/store/memory').Memory({path: 'response'}), //require('perstore/store/filesystem').FileSystem('response', {defaultExtension: 'cache',dataFolder: 'cache' }),
serverName: 'Pintura',
customRoutes: [],
getDataModel: function(request){
groups: {
public: [null],
user: '*',
admin: ['admin'],
},
getDataModel: function(request){
return exports.getDataModel(request);
}
};
exports.getDataModel = function(){
throw new Error('You must assign a getDataModel method to the pintura config object in order to expose data');
exports.configure = function(newConfig){
// copy new configuration options into the config object
deepCopy(newConfig, config);
}
exports.getDataModel = function(request){
// this is a simple default model
return request.dataModel;
};
exports.registerModels = config.security.registerModels;

exports.app = configure([
// This is the set of JSGI middleware and appliance that comprises the Pintura
// request handling framework.
Expand All @@ -55,6 +68,8 @@ exports.app = configure([
{module: './session', config: {}},
// Do authentication
{module: './auth', config: config.security},
// Determine access to data models
{module: './access', config: config.security},
// Handle request conneg, converting from byte representations to JS objects
{factory: require('./jsgi/media').Deserialize, config: config.mediaSelector},
// Non-REST custom handlers
Expand Down
84 changes: 83 additions & 1 deletion security.js
Expand Up @@ -8,7 +8,8 @@ var AccessError = require("perstore/errors").AccessError,
getCurrentSession = require("./jsgi/session").getCurrentSession,
Restrictive = require("perstore/facet").Restrictive,
sha1 = require("./util/sha1").b64_sha1,
settings = require("perstore/util/settings");
settings = require("perstore/util/settings"),
modelModule = require("perstore/model");

try{
var uuid = require("uuid");
Expand Down Expand Up @@ -49,7 +50,9 @@ exports.DefaultSecurity = function(){

var userModel;
var admins = settings.security && settings.security.admins;
var groupToModels = {};
var security = {
// authentication methods:
encryptPassword: function(username, password){
return password && sha1(password);
},
Expand Down Expand Up @@ -113,6 +116,85 @@ exports.DefaultSecurity = function(){
},
setUserModel: function(value){
userModel = value;
},

// access methods:
groupsUsers: {
user: ['*'],
public: [null]
},
getGroupsForUser: function(user){
var groupsUsers = this.groupsUsers;
var groupsForUser = [];
// at some point we may want to convert this to an array for faster access
for(var groupName in groupsUsers){
var users = groupsUsers[groupName];
if(users.indexOf(user) > - 1 || users.indexOf('*') > - 1){
// the user is in this group
groupsForUser.push(groupName);
}
}
return groupsForUser;
},
modelForUsers: {},
getModelForUser: function(user){
if(this.modelForUsers[user]){
// check the cache
return this.modelForUsers[user];
}
var groups = this.getGroupsForUser(user).concat(['_default']);
var model = {};
groups.forEach(function(group){
var groupModel = groupToModels[group];
for(var key in groupModel){
if(!model[key] || !(model[key].quality > groupModel[key])){
model[key] = groupModel[key];
}
}
});

this.modelForUsers[user] = model;
modelModule.initializeRoot(model);
return model;
},
registerModels: function(){
var groups;
processItem([].slice.call(arguments));
function processItem(item, key){
if(item instanceof Array){
// process each item in an array
item.forEach(function(item){
processItem(item, key);
});
}else if(item && item.groups && typeof item === 'object'){
// define the groups and recurse
groups = item.groups;
if(item.model){
if(typeof item.model !== 'function'){
throw new Error('Model in ' + key + ' does not appear to be a valid model constructor' + item.model);
}
}else if(!item.models){
throw new Error('No valid model provided for ' + key + ' with groups ' + groups);
}
processItem(item.models || item.model, key);
groups = null;
}else if(typeof item === 'function'){
// a model itself, add the definition
if(!key){
throw new Error('No key defined for model');
}
(groups || item.groups || ['_default']).forEach(function(group){
(groupToModels[group] || (groupToModels[group] = {}))[key] = item;
});
}else if(item && typeof item === 'object'){
// an object hash, process each as a key
for(key in item){
processItem(item[key], key);
}
}else{
throw new Error('An invalid value encountered in model registration ' + item + ' for groups ' + groups + ' for name ' + key);
}
}
}
};
exports.userSchema.authenticate = authenticate;
Expand Down

0 comments on commit ae765b4

Please sign in to comment.