Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authorizations and Access Control to hide API methods and check access #14

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 98 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,108 @@ node-restify-swagger
[![Coverage Status](https://coveralls.io/repos/z0mt3c/node-restify-swagger/badge.png?branch=master)](https://coveralls.io/r/z0mt3c/node-restify-swagger?branch=master)
[![Dependency Status](https://gemnasium.com/z0mt3c/node-restify-swagger.png)](https://gemnasium.com/z0mt3c/node-restify-swagger)

Swagger resource generation for [Restify](https://github.com/mcavage/node-restify).

Install
Uses [node-restify-validation](https://github.com/z0mt3c/node-restify-validation) for query, path, body validations.

Includes an optional access control scheme which allows api methods to be hidden and access granted from main application.


Installation
-------

Install the module.

npm install node-restify-swagger


Include a copy of [Swagger-UI](https://github.com/swagger-api/swagger-ui) at the root of your project.


Demo project
-------

A simple demo project can be cloned from [node-restify-demo](https://github.com/z0mt3c/node-restify-demo).


Sample App.js
-------

var restify = require('restify');
var restifyValidation = require('node-restify-validation');
var restifySwagger = require('node-restify-swagger');

var server = module.exports.server = restify.createServer();
server.use(restify.queryParser());
server.use(restify.bodyParser());
server.use(restifyValidation.validationPlugin({ errorsAsArray: false }));

restifySwagger.configure(server, {
info: {
contact: '[email protected]',
description: 'Description text',
license: 'MIT',
licenseUrl: 'http://opensource.org/licenses/MIT',
termsOfServiceUrl: 'http://opensource.org/licenses/MIT',
title: 'Node Restify Swagger Demo'
},
apiDescriptions: {
'get':'GET-Api Resourcen'
}
});

// accessControl function which will be used by the restifySwagger middleware
// and resource generation functionality to authorize access to the methods (optional)
function accessControl(req, res) {

var api_key = '';
if (typeof req.params.api_key == 'string') {
api_key = req.params.api_key;
}

// if api_key is valid then return a space seperated list of authorizations
// the authorizations will be defined on the routes
return ((api_key == '12345') ? 'public account' : '');

}

// verify authentication and authorization (optional)
restifySwagger.authorizeAccess(accessControl);

// Test Controller
server.get({url: '/get/:name',
authorizations: 'account',
swagger: {
summary: 'My hello call description',
notes: 'My hello call notes',
nickname: 'sayHelloCall'
},
validation: {
name: { isRequired: true, isIn: ['foo', 'bar'], scope: 'path', description: 'Your unreal name' },
status: { isRequired: true, isIn: ['foo', 'bar'], scope: 'query', description: 'Are you foo or bar?' },
email: { isRequired: false, isEmail: true, scope: 'query', description: 'Your real email address' },
age: { isRequired: true, isInt: true, scope: 'query', description: 'Your age' },
accept: { isRequired: true, isIn: ['true', 'false'], scope: 'query', swaggerType: 'boolean', description: 'Are you foo or bar?' },
password: { isRequired: true, description: 'New password' },
passwordRepeat: { equalTo: 'password', description: 'Repeated password'}
}}, function (req, res, next) {
res.send(req.params);
});

// Serve static swagger resources
server.get(/^\/docs\/?.*/, restify.serveStatic({directory: './swagger-ui'}));
server.get('/', function (req, res, next) {
res.header('Location', '/docs/index.html');
res.send(302);
return next(false);
});

restifySwagger.loadRestifyRoutes(accessControl);

// Start server
server.listen(8001, function () {
console.log('%s listening at %s', server.name, server.url);
});


License
-------
Expand Down
33 changes: 28 additions & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ module.exports.configure = function (server, options) {
swagger.configure(this.server, this.options);
};

module.exports.findOrCreateResource = function (resource, options) {
module.exports.findOrCreateResource = function (resource, options, accessControl) {
assert.ok(swagger.resources, 'Swagger not initialized! Execution of configure required!');

var found = _.find(swagger.resources, function (myResource) {
Expand All @@ -75,7 +75,7 @@ module.exports.findOrCreateResource = function (resource, options) {
_.extend(found.models, options.models);
}

var docs = found || swagger.createResource(resource, options || { models: {}, description: getApiDescription(resource) });
var docs = found || swagger.createResource(resource, options || { models: {}, description: getApiDescription(resource) }, accessControl);
return docs;
};

Expand Down Expand Up @@ -107,8 +107,28 @@ var extractSubtypes = module.exports._extractSubtypes = function (model, swagger
});
};

// middleware to check access and authorizations
module.exports.authorizeAccess = function(accessControl) {

module.exports.loadRestifyRoutes = function () {
this.server.use(function(req, res, next) {

var userAuthorizations = swagger.getUserAuthorizations(req, res, accessControl);

var routeAuthorizations = swagger.getRouteAuthorizations(req.route.authorizations);

if (swagger.checkAuthorized(userAuthorizations, routeAuthorizations)) {
return next();
}
else {
var restify = require('restify');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't require inline and remember restify is(was) a dev dependency.

return next(new restify.NotAuthorizedError('Not Authorized'));
}

});

};

module.exports.loadRestifyRoutes = function (accessControl) {
var self = this;

_.each(this.server.router.mounts, function (item) {
Expand All @@ -124,8 +144,10 @@ module.exports.loadRestifyRoutes = function () {
var mySwaggerPath = module.exports.swaggerPathPrefix + mySwaggerPathParts;
var models = spec.models || {};

var authorizations = ((typeof spec.authorizations === 'string') ? { type: spec.authorizations } : {});

if (!_.contains(self.options.blacklist, mySwaggerPathParts)) {
var swaggerDoc = self.findOrCreateResource(mySwaggerPath, { models: models, description: getApiDescription(mySwaggerPath) });
var swaggerDoc = self.findOrCreateResource(mySwaggerPath, { models: models, description: getApiDescription(mySwaggerPath) }, accessControl);
var parameters = [];
var modelName = name;
var model = { properties: { } };
Expand Down Expand Up @@ -216,7 +238,8 @@ module.exports.loadRestifyRoutes = function () {
message: 'Internal Server Error'
}
],
parameters: parameters
parameters: parameters,
authorizations: authorizations
});
}
}
Expand Down
188 changes: 173 additions & 15 deletions lib/swagger-doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,183 @@ swagger.configure = function(server, options) {
* Registers a Resource with the specified path and options.
* @param {!String} path The path of the resource.
* @param {{models}} options Optional options that can contain models.
* @param {Function} accessControl User defined access control function for verifying api method access
* @return {Resource} The new resource.
*/
swagger.createResource = function(path, options) {
var resource = new Resource(path, options),
self = this;
this.resources.push(resource);
swagger.createResource = function(path, options, accessControl) {

this.server.get(path, function(req, res) {
var result = self._createResponse(req);
result.resourcePath = path;
result.apis = Object.keys(resource.apis).map(function(k) { return resource.apis[k]; });
result.models = resource.models;
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, PATCH, POST, DELETE, PUT');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.send(result);
});
// createResource is called when the loadRestifyRoutes is executed during the app load

var resource = new Resource(path, options), self = this;

this.resources.push(resource);

// this function is called when the user loads the swagger ui
this.server.get(path, function(req, res) {

// GET route is called when Swagger UI access one of the resource routes

// create the userAuthorizations array
var userAuthorizations = self.getUserAuthorizations(req, res, accessControl);

// createResponse will generate information about the swagger version, api version, and licensing
var result = self._createResponse(req);
result.resourcePath = path;

// the following arrays will contains the apis (path) and methods (name) that are authorized
var apisAllowed = [];
var modelsAllowed = [];

// loop through the api methods and determine what api methods and methods are allowed
for (var obj in resource.apis) {
// api has operations property
if (resource.apis.hasOwnProperty(obj)) {

// the path parameter above is /swagger/worker
// the api_path will change for each method in the api
// example: /worker/new, /worker/id/{id}, etc.
var api_path = '';
if (typeof resource.apis[obj].path === 'string') {
api_path = resource.apis[obj].path;
}

// check the new 'authorizations' route property
var routeAuthorizations = self.getRouteAuthorizations(resource.apis[obj].operations[0].authorizations.type);

// if the authorization type defined in the route
var apiAllowed = self.checkAuthorized(userAuthorizations, routeAuthorizations);
if (apiAllowed) {
apisAllowed.push(api_path);
}

// check api has parameters object
if (typeof resource.apis[obj].operations[0].parameters === 'object') {

// loop through the parameters
for (var i = 0; i < Object.keys(resource.apis[obj].operations[0].parameters).length; i++) {
// parameters has valid object
if (typeof resource.apis[obj].operations[0].parameters[i] === 'object') {

// check to see if we found an api that should have a model
if ((resource.apis[obj].operations[0].parameters[i].paramType === 'body') &&
(typeof resource.apis[obj].operations[0].parameters[i].dataType !== 'undefined')) {

// if the api is allowed then include the model
if (apiAllowed) {
modelsAllowed.push(resource.apis[obj].operations[0].parameters[i].dataType);
}
}
}
}
}

}
}

// populate result.apis with only the authenticated or public api methods
var apis = Object.keys(resource.apis).map(function(k) { return resource.apis[k]; });
result.apis = [];
for (var api in apis) {
if (apis.hasOwnProperty(api)) {
if (apisAllowed.indexOf(apis[api].path) !== -1) {
result.apis.push(apis[api]);
}
}
}

// populate the models with only those models that are related to authenticated apis
result.models = {};
for (var model in resource.models) {
if (resource.models.hasOwnProperty(model)) {
if (modelsAllowed.indexOf(model) !== -1) {
result.models[model] = resource.models[model];
}
}
}

res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, PATCH, POST, DELETE, PUT');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.send(result);
});

return resource;
};

swagger.cleanAuthorizations = function(str) {

var clean = str;

// replace commas in the authorizations as spaces
clean = clean.replace(',', ' ');
// replace spaces in the authorizations with single spaces
clean = clean.replace(/ +/g, ' ');
// make the string lowercase for future comparisons
clean = clean.toLowerCase();

return clean;

};

swagger.getUserAuthorizations = function(req, res, accessControl) {

// create the userAuthorizations array with the public default
var userAuthorizations = ['public'];

// verify that the api_key is provided and the accessControl method was passed to the
if ((typeof req.params.api_key !== 'undefined') && (typeof accessControl === 'function')) {

// call the user defined access control function and get back the userAuthorizations
var userAuths = accessControl(req, res);
if (userAuths.length) {
// clean the authorizations string
userAuths = swagger.cleanAuthorizations(userAuths);
// split the space delimited array of authorizations into an array
userAuthorizations = userAuths.split(' ');
}

}

return userAuthorizations;

};

swagger.getRouteAuthorizations = function(authorization_type) {

// check the new 'authorizations' route property
var routeAuthorizations = [];

if (typeof authorization_type === 'string') {
var routeAuths = swagger.cleanAuthorizations(authorization_type);
routeAuthorizations = routeAuths.split(' ');
}
else {
// make api methods without 'authorizations' work be assigning default of public
routeAuthorizations = ['public'];
}

return routeAuthorizations;

};

swagger.checkAuthorized = function(userAuthorizations, routeAuthorizations) {

var result = false;

// loop through all of the route authorizations
for (var routeAuthorization in routeAuthorizations) {
if (routeAuthorizations.hasOwnProperty(routeAuthorization)) {
// check to see if the user is authorized
if (userAuthorizations.indexOf(routeAuthorizations[routeAuthorization]) !== -1) {
// the api is allowed
result = true;
// exist the for loop
break;
}
}
}

return resource;
return result;
};

swagger._createResponse = function(req) {
Expand Down