Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 6 commits
  • 17 files changed
  • 4 commit comments
  • 3 contributors
Commits on May 20, 2014
@kriszyp kriszyp Added new configure middleware that can configured a JSGI stack based…
… on array, and modify configurations
0c67f88
Commits on Jul 01, 2014
@kriszyp kriszyp Added new access handling with the ability determine model based on g…
…roup membership
f48b723
Commits on Sep 24, 2014
@neonstalwart neonstalwart remove websocket-server
working on replacing it with socket.io
164737d
@neonstalwart neonstalwart switch to use ws instead of websocket-server
websocket-server is unpublished so updating to an alternative
96053e2
Commits on Sep 25, 2014
@neonstalwart neonstalwart account for websocket requests in csrf b877156
Commits on Dec 22, 2014
@deanlandolt deanlandolt Add test cmd to package.json fc5ee8f
View
184 README.md
@@ -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.
@@ -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
@@ -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
===========
@@ -467,16 +512,64 @@ manipulate these JSGI modules unless you want to customize or alter the middlewa
stack. The pintura module provides all these middleware components with a default working
setup that be immediately used without any knowledge of the middleware components
described below. However, understanding the middleware modules can be important
-in understanding the full capabilities of Pintura.
+in understanding the full capabilities of Pintura, and for reconfiguring the middleware.
+
+Once you have defined a JSGI app, you can start your server with that app like:
+
+ require("pintura/start-node").start(app);
The middleware modules in Pintura
are found in the "jsgi" folder. Most of these modules directly a function that can be
used as the middleware function, and typically take configuration information as the
first parameter and the next application as the second. Below are the syntax and description of these modules:
+## configure
+
+The Pintura middleware stack is defined by using the `configure` module. This is a function
+that takes an array of middleware definitions and creates the stack for you. Once the middleware
+stack has been created, you can also modify the stack, using standard array methods. For example,
+to create a stack of middleware, we could write:
+
+ require('pintura/jsgi/configure');
+ configuredApp = configure([
+ { // we can define middleware by referencing the module
+ module: 'pintura/jsgi/auth',
+ // and providing a config
+ config: security
+ },
+ 'pintura/jsgi/csrf' // or just use a module id directly
+ ]);
+
+Each middleware entry in the stack can be defined as one of these:
+
+* An object with a module id in the `module` property, or a factory function in the `factory` property. You can optionally include a `config` property if the middleware takes a config in its arguments.
+* A plain module id.
+* A factory function.
+
+The return app will then have standard array methods available for modifying the stack. Any changes to the stack will cause it automatically rebuild itself. In addition, there are several extra methods:
+* get(id) - Get the configuration for a middleware by the module's last segment (in the case of 'pintura/jsgi/csrf', the id would 'csrf'), or the function name.
+* set(id, config) - Update the configuration of one of the middleware.
+* delete(id) - Remove a middleware component from the stack.
+
+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
@@ -487,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
View
10 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);
+ };
+};
View
151 jsgi/configure.js
@@ -0,0 +1,151 @@
+/**
+ * Middleware for configuring middleware with a fluent API
+ */
+var NotFoundError = require('perstore/errors').NotFoundError;
+
+module.exports = function(configuration){
+ var root;
+ function configuredApp(request){
+ // delegate to the root, so we can rebuild the root,
+ // if and when we reconfigure
+ return root(request);
+ }
+ function end(request){
+ throw new NotFoundError('unhandled request');
+ }
+ var configurationMap;
+ function build(){
+ configurationMap = {};
+ var current = end;
+ var appConfig;
+ try{
+ for(var i = configuration.length; i-- > 0;){
+ appConfig = configuration[i];
+ var factory = null;
+ var configArgument = null;
+ if(typeof appConfig === 'function'){
+ factory = appConfig;
+ appConfig = {
+ factory: factory
+ };
+ }else if(typeof appConfig === 'string'){
+ factory = require(appConfig);
+ appConfig = {
+ factory: factory,
+ module: appConfig
+ };
+ }else if(typeof appConfig.module === 'string'){
+ factory = require(appConfig.module);
+ configArgument = appConfig.config;
+ }else if(typeof appConfig.factory === 'function'){
+ factory = appConfig.factory;
+ configArgument = appConfig.config;
+ }else{
+ throw new Error('Invalid configuration, no app or module provided ' + JSON.stringify(appConfig));
+ }
+ configuration[i] = appConfig;
+ configurationMap[getId(appConfig)] = appConfig;
+ current = appConfig.app = configArgument === null ?
+ factory(current) :
+ // it needs a configuration
+ factory(configArgument, current);
+ if(typeof current !== 'function'){
+ throw new Error('JSGI app factory did not return a function');
+ }
+ }
+ }catch(error){
+ console.error('failed to configure JSGI app', appConfig, error);
+ throw error;
+ }
+ root = current;
+ }
+
+ function getId(config){
+ var module = typeof config === 'string' ?
+ config : config.module;
+ if(module){
+ return module.match(/[^\/]+$/)[0];
+ }
+ return (config.factory || config.app).name;
+ }
+
+ build();
+ function reconfigure(newConfiguration){
+ configuration = newConfiguration;
+ build();
+ }
+ configuredApp.reconfigure = reconfigure;
+
+ // create delegate array methods
+ function arrayMethod(mutates){
+ return function(method){
+ configuredApp[method] = function(){
+ // delegate the method to the configuration array
+ var result = [][method].apply(configuration, arguments);
+ if(mutates){
+ // rebuild after any change was made
+ build();
+ }
+ return result;
+ };
+ };
+ }
+ ['push', 'pop', 'unshift', 'shift', 'splice', ''].forEach(arrayMethod(true));
+ ['slice', 'forEach', 'map', 'reduce', 'reduceRight'].forEach(arrayMethod());
+ ['filter', 'every', 'some'].forEach(function(method){
+ configuredApp[method] = function(callback, thisObject){
+ if(typeof callback == 'string'){
+ // convert string search to a search by name
+ var nameToSearch = callback;
+ callback = function(item){
+ return getId(item) === nameToSearch;
+ };
+ }
+ // delegate the method to the configuration array
+ return configuration[method](callback, thisObject);
+ };
+ });
+ configuredApp.indexOf = function(searchFor, starting){
+ if(typeof searchFor === 'string'){
+ for(var i = starting || 0, l = configuration.length; i < l; i++){
+ if(getId(configuration[i]) === searchFor){
+ return i;
+ }
+ }
+ return -1;
+ }
+ return configuration.indexOf(searchFor, starting);
+ };
+ configuredApp.lastIndexOf = function(searchFor, starting){
+ if(typeof searchFor === 'string'){
+ for(var i = starting || configuration.length; i-- > 0;){
+ if(getId(configuration[i]) === searchFor){
+ return i;
+ }
+ }
+ return -1;
+ }
+ return configuration.lastIndexOf(searchFor, starting);
+ };
+ configuredApp.get = function(id){
+ return configuredApp.filter(id)[0];
+ };
+ configuredApp.set = function(id, config){
+ var index = configuredApp.indexOf(id);
+ if(index > -1){
+ configuredApp.splice(index, 1, config);
+ }else{
+ configuredApp.push(config);
+ }
+ };
+ configuredApp.delete = function(id){
+ var index = configuredApp.indexOf(id);
+ if(index > -1){
+ configuredApp.splice(index, 1);
+ }
+ };
+ configuredApp.asArray = function(){
+ return configuration;
+ };
+ return configuredApp;
+};
View
4 jsgi/csrf.js
@@ -5,12 +5,14 @@
var CSRFDetect = function(customHeader, nextApp){
if(typeof customHeader == "function"){
nextApp = customHeader;
+ // default to this common header
+ customHeader = "x-requested-with";
}
- customHeader = customHeader || "x-requested-with";
return function(request){
var headers = request.headers;
if(!(headers[customHeader] || /application\/j/.test(headers.accept) ||
(request.method == "POST" && headers.referer && headers.referer.indexOf(headers.host + '/') > 0) ||
+ (headers.stream && headers.origin && headers.origin.indexOf(headers.host) > 0) ||
(request.method != "GET" && request.method != "POST"))){
request.crossSiteForgeable = true;
}
View
4 jsgi/error.js
@@ -9,7 +9,7 @@ var METHOD_HAS_BODY = require("./methods").METHOD_HAS_BODY,
print = require("promised-io/process").print,
when = require("promised-io/promise").when;
-exports.ErrorHandler = function(nextApp){
+exports = module.exports = function(nextApp){
return function(request){
try{
return when(nextApp(request), function(response){
@@ -73,3 +73,5 @@ exports.ErrorHandler = function(nextApp){
}
};
};
+
+exports.ErrorHandler = exports;
View
4 jsgi/http-params.js
@@ -5,7 +5,7 @@
* setting window.location to download data).
*/
var httpParamRegex = /^http[_-]/;
-exports.HttpParams = function(nextApp){
+exports = module.exports = function(nextApp){
return function(request){
var parts = request.queryString.split("&");
@@ -33,3 +33,5 @@ exports.HttpParams = function(nextApp){
return nextApp(request);
};
};
+// back-compat property reference
+exports.HttpParams = exports;
View
6 jsgi/metadata.js
@@ -2,7 +2,7 @@
* Applies metadata headers if the responseValue includes a getMetadata function
*/
var when = require("promised-io/promise").when;
-exports.Metadata = function(nextApp){
+module.exports = function(nextApp){
return function(request){
var metadata;
var input = request.body || '';
@@ -53,7 +53,9 @@ exports.Metadata = function(nextApp){
return response;
});
};
-}
+};
+// back-compat property access
+module.exports.Metadata = module.exports;
/*
var lastModified = callIfPossible(store, "getLastModified", value);
View
6 jsgi/rest-store.js
@@ -10,10 +10,8 @@ var METHOD_HAS_BODY = require("./methods").METHOD_HAS_BODY,
Response = require("./response").Response,
settings = require("perstore/util/settings");
-function dir(){var sys=require('sys');for(var i=0,l=arguments.length;i<l;i++)sys.debug(sys.inspect(arguments[i]));}
-exports.RestStore = function(options){
+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;
@@ -163,3 +161,5 @@ exports.RestStore = function(options){
});
};
};
+// back-compat property access
+module.exports.RestStore = module.exports;
View
5 jsgi/routes.js
@@ -14,7 +14,7 @@ var when = require('promised-io/promise').when,
var routes = [];
//function dir(){var sys=require('sys');for(var i=0,l=arguments.length;i<l;i++)sys.debug(sys.inspect(arguments[i]));}
-exports.Routes = function(customRoutes, nextApp){
+exports = module.exports = function(customRoutes, nextApp){
// append custom routes
for (var i = 0, l = customRoutes.length; i < l; ++i) {
declare(customRoutes[i]);
@@ -57,7 +57,8 @@ exports.Routes = function(customRoutes, nextApp){
return nextApp(request);
};
};
-
+// back-compat property access
+exports.Routes = exports;
function declare(method, regexp, handler, args){
if (typeof regexp === 'string') {
var named_param_regex = /[\/\.]:(\w+)/g;
View
5 jsgi/session.js
@@ -11,7 +11,7 @@ var promiseModule = require("promised-io/promise"),
sessionModel,
sha1 = require("../util/sha1").hex_sha1;
-exports.Session = function(options, nextApp){
+exports = module.exports = function(options, nextApp){
// assign defaults
if (!options) options = {};
if(options.model){
@@ -67,7 +67,8 @@ exports.Session = function(options, nextApp){
});
};
};
-
+// back-compat property reference
+exports.Session = exports;
// gets a session, creating a new one if necessary
exports.forceSession = function(request, expires){
var session = request.session;
View
51 package.json
@@ -4,15 +4,17 @@
"author": "Kris Zyp",
"email": "kriszyp@gmail.com",
"description": "JSGI-based RESTful JSON/JavaScript server",
- "contributors": ["Vladimir Dronnikov <dronnikov@gmail.com>"],
+ "contributors": [
+ "Vladimir Dronnikov <dronnikov@gmail.com>"
+ ],
"keywords": [
"rest",
"database",
"web",
"json",
"persevere"
- ],
- "mappings":{
+ ],
+ "mappings": {
"perstore": "http://github.com/kriszyp/perstore/zipball/v0.2.4",
"templify": "http://github.com/dmachi/templify/zipball/master",
"promised-io": "jar:http://github.com/kriszyp/promised-io/zipball/v0.2.3!/",
@@ -35,39 +37,42 @@
}
},
"licenses": [
- {
- "type": "AFLv2.1",
- "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43"
- },
- {
- "type": "BSD",
- "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13"
- }
+ {
+ "type": "AFLv2.1",
+ "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43"
+ },
+ {
+ "type": "BSD",
+ "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13"
+ }
],
"repository": {
- "type":"git",
- "url":"http://github.com/persvr/pintura"
+ "type": "git",
+ "url": "http://github.com/persvr/pintura"
},
"directories": {
"lib": "."
},
"maintainers": [
- {
- "name": "Kris Zyp",
- "email": "kriszyp@gmail.com"
- }
+ {
+ "name": "Kris Zyp",
+ "email": "kriszyp@gmail.com"
+ }
],
- "dependencies":{
- "tunguska": ">=0.3.0",
- "rql": ">=0.3.1",
- "websocket-server": ">=1.4.01",
+ "dependencies": {
+ "formidable": ">=1.0.0",
+ "jsgi-node": ">=0.2.5",
"perstore": ">=0.3.0",
"promised-io": ">=0.3.0",
- "formidable": ">=1.0.0",
+ "rql": ">=0.3.1",
"templify": ">=0.9.0",
- "jsgi-node": ">=0.2.5"
+ "tunguska": ">=0.3.0",
+ "ws": "^0.4.32"
},
"devDependencies": {
"patr": ">=0.2.6"
+ },
+ "scripts": {
+ "test": "cd tests; node ."
}
}
View
142 pintura.js
@@ -4,89 +4,89 @@
* and defining the default mechanisms for security, content negotiation, and persistence.
*/
// load the default media types
-require("./media/json");
-require("./media/javascript");
-require("./media/url-encoded");
-require("./media/atom");
-require("./media/multipart-form-data");
-require("./media/html");
-require("./media/uri-list");
-require("./media/plain");
-require("./media/message/json");
+require('./media/json');
+require('./media/javascript');
+require('./media/url-encoded');
+require('./media/atom');
+require('./media/multipart-form-data');
+require('./media/html');
+require('./media/uri-list');
+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",
+ 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.app = JsgiApp(null, config);
+exports.registerModels = config.security.registerModels;
-function JsgiApp(nextApp, config){
+exports.app = configure([
// This is the set of JSGI middleware and appliance that comprises the Pintura
// request handling framework.
- return require("./jsgi/context").SetContext({},
- // We detect if the request could have been forged from another site
- require("./jsgi/csrf").CSRFDetect(
- // Support handling various cross-site request mechanisms like JSONP, window.name, CS-XHR
- require("./jsgi/xsite").CrossSite(
- // Handle header emulation through query parameters (useful for cross-site and links)
- require("./jsgi/http-params").HttpParams(
- // Handle HEAD requests
- require("./jsgi/head").Head(
- // Add some useful headers
- require("./jsgi/pintura-headers").PinturaHeaders(config.serverName,
- // Handle conditional requests
- require("./jsgi/conditional").Conditional(true,
- // Handle response conneg, converting from JS objects to byte representations
- require("./jsgi/media").Serialize(config.mediaSelector,
- // Handle errors that are thrown, converting to appropriate status codes
- require("./jsgi/error").ErrorHandler(
- // Handle transactions
- require("perstore/jsgi/transactional").Transactional(
- // Handle sessions
- require("./jsgi/session").Session({},
- // Do authentication
- require("./jsgi/auth").Authentication(config.security,
- // Handle request conneg, converting from byte representations to JS objects
- require("./jsgi/media").Deserialize(config.mediaSelector,
- // Non-REST custom handlers
- require('./jsgi/routes').Routes(config.customRoutes,
- // Add and retrieve metadata from objects
- exports.directApp = require("./jsgi/metadata").Metadata(
- // Final REST handler
- require("./jsgi/rest-store").RestStore(config)
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- )
- );
-};
-exports.JsgiApp = JsgiApp;
-var Connector = require("tunguska/connector").Connector;
+ {module: './context', config: {}},
+ // We detect if the request could have been forged from another site
+ './csrf',
+ // Support handling various cross-site request mechanisms like JSONP, window.name, CS-XHR
+ './xsite',
+ // Handle header emulation through query parameters (useful for cross-site and links)
+ './http-params',
+ // Handle HEAD requests
+ './head',
+ // Add some useful headers
+ {module: './pintura-headers', config: config.serverName},
+ // Handle conditional requests
+ {module: './conditional', config: true},
+ // Handle response conneg, converting from JS objects to byte representations
+ {factory: require('./jsgi/media').Serialize, config: config.mediaSelector},
+ // Handle errors that are thrown, converting to appropriate status codes
+ './error',
+ // Handle transactions
+ 'perstore/jsgi/transactional',
+ // Handle sessions
+ {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
+ {module: './routes', config: config.customRoutes},
+ // Add and retrieve metadata from objects
+ './metadata',
+ // Final REST handler
+ {module: './rest-store', config: config}
+ ]);
+
+
+var Connector = require('tunguska/connector').Connector;
exports.addConnection = exports.app.addConnection = function(connection){
- Connector("local-workers", connection);
- connection[connection.on ? "on" : "observe"]("message", function(message){
+ Connector('local-workers', connection);
+ connection[connection.on ? 'on' : 'observe']('message', function(message){
message.pathInfo = message.channel || message.to;
- if(message.method && message.method !== "subscribe"){
+ if(message.method && message.method !== 'subscribe'){
exports.directApp(message);
}
});
View
84 security.js
@@ -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");
@@ -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);
},
@@ -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;
View
8 start-node.js
@@ -1,6 +1,6 @@
// helpful for debugging
var settings = require("perstore/util/settings"),
- ws = require("websocket-server"),
+ ws = require("ws"),
messageJson = require("./media/message/json");
exports.start = function(jsgiApp){
@@ -9,9 +9,7 @@ exports.start = function(jsgiApp){
);
var port = settings.port || process.env.PORT || 80;
server.listen(port);
- require("jsgi-node/ws-jsgi")(ws.createServer({
- server: server
- }), function(request){
+ require("jsgi-node/ws-jsgi")(ws.createServer({ server: server }), function(request){
request.method = "POST";
var headers = request.headers;
headers.accept = "message/json";
@@ -19,7 +17,7 @@ exports.start = function(jsgiApp){
headers.stream = true;
return jsgiApp(request);
});
-
+
console.log("Listening on port " + port);
return server;
};
View
2  tests/index.js
@@ -1,4 +1,4 @@
-exports.testFullRest = require("./rest");
+exports.testConfigure = require("./jsgi/configure");
exports.testJSGIMiddleware = require("./jsgi/index");
if (require.main === module)
View
50 tests/jsgi/configure.js
@@ -0,0 +1,50 @@
+var configure = require('../../jsgi/configure'),
+ assert = require('assert');
+
+
+exports.testConfigure = function(){
+ var topCalled;
+ var configuredApp = configure([
+ './xsite',
+ {module: './pintura-headers', config: 'test'},
+ {factory: function test(nextApp){
+ return function(request){
+ topCalled = true;
+ return {headers: {}};
+ };
+ }}
+ ]);
+ var request = {
+ queryString: '',
+ headers: {}
+ };
+ var appArray = configuredApp.asArray();
+ assert.equal(appArray.length, 3);
+ assert.equal(typeof appArray[0].app, 'function');
+ assert.equal(appArray[0].module, './xsite');
+ assert.equal(typeof appArray[1].app, 'function');
+ assert.equal(typeof appArray[2].app, 'function');
+ configuredApp(request);
+ assert.equal(topCalled, true);
+ assert.equal(configuredApp.get('xsite').module, './xsite');
+ configuredApp.delete('xsite');
+ appArray = configuredApp.asArray();
+ assert.equal(appArray.length, 2);
+ configuredApp.unshift('./head');
+ appArray = configuredApp.asArray();
+ assert.equal(appArray.length, 3);
+ topCalled = false;
+ configuredApp(request);
+ assert.equal(topCalled, true);
+ configuredApp.delete('test');
+ topCalled = false;
+ try{
+ configuredApp(request);
+ }catch(e){
+ // this will error because there is no handler
+ }
+ assert.equal(topCalled, false);
+};
+
+if (require.main === module)
+ require('patr/runner').run(exports);
View
1  tests/jsgi/index.js
@@ -1,4 +1,5 @@
exports.testCSRF = require("./csrf");
+exports.testConfigure = require("./csrf");
if (require.main === module)
require("patr/runner").run(exports);

Showing you all comments on commits in this comparison.

@neonstalwart

@kriszyp do you have a local change to perstore/jsgi/transaction that makes this work like this?

@kriszyp
Owner

Oh, yes, let me check that in.

@kriszyp
Owner

I just checked that in.

@neonstalwart

thanks. took me a minute to find it... i was looking at persvr/perstore but i see it now at kriszyp/perstore

Something went wrong with that request. Please try again.