diff --git a/common/models/role.js b/common/models/role.js index b8f36317..daa7505f 100644 --- a/common/models/role.js +++ b/common/models/role.js @@ -13,8 +13,9 @@ var Application = loopback.Application; assert(RoleMapping, 'RoleMapping model must be defined before Role model'); /** - * The Role Model + * The Role model * @class Role + * @header Role object */ module.exports = function(Role) { @@ -139,7 +140,7 @@ module.exports = function(Role) { } /*! - * Check if two user ids matches + * Check if two user IDs matches * @param {*} id1 * @param {*} id2 * @returns {boolean} @@ -227,11 +228,12 @@ module.exports = function(Role) { }); /** - * Check if the user id is authenticated - * @param {Object} context The security context - * @callback {Function} callback - * @param {Error} err - * @param {Boolean} isAuthenticated + * Check if the user ID is authenticated + * @param {Object} context The security context. + * + * @callback {Function} callback Callback function. + * @param {Error} err Error object. + * @param {Boolean} isAuthenticated True if the user is authenticated. */ Role.isAuthenticated = function isAuthenticated(context, callback) { process.nextTick(function() { @@ -252,13 +254,14 @@ module.exports = function(Role) { }); /** - * Check if a given principal is in the role + * Check if a given principal is in the specified role. * - * @param {String} role The role name - * @param {Object} context The context object - * @callback {Function} callback - * @param {Error} err - * @param {Boolean} isInRole + * @param {String} role The role name. + * @param {Object} context The context object. + * + * @callback {Function} callback Callback function. + * @param {Error} err Error object. + * @param {Boolean} isInRole True if the principal is in the specified role. */ Role.isInRole = function(role, context, callback) { if (!(context instanceof AccessContext)) { @@ -343,13 +346,12 @@ module.exports = function(Role) { }; /** - * List roles for a given principal - * @param {Object} context The security context - * @param {Function} callback + * List roles for a given principal. + * @param {Object} context The security context. * - * @callback {Function} callback - * @param {Error=} err - * @param {String[]} roles An array of role ids + * @callback {Function} callback Callback function. + * @param {Error} err Error object. + * @param {String[]} roles An array of role IDs */ Role.getRoles = function(context, callback) { if (!(context instanceof AccessContext)) { diff --git a/server/middleware/token.js b/server/middleware/token.js index 72eb340e..f23da1c4 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -4,6 +4,7 @@ var loopback = require('../../lib/loopback'); var assert = require('assert'); +var debug = require('debug')('loopback:middleware:token'); /*! * Export the middleware. @@ -11,6 +12,27 @@ var assert = require('assert'); module.exports = token; +/* + * Rewrite the url to replace current user literal with the logged in user id + */ +function rewriteUserLiteral(req, currentUserLiteral) { + if (req.accessToken && req.accessToken.userId && currentUserLiteral) { + // Replace /me/ with /current-user-id/ + var urlBeforeRewrite = req.url; + req.url = req.url.replace( + new RegExp('/' + currentUserLiteral + '(/|$|\\?)', 'g'), + '/' + req.accessToken.userId + '$1'); + if (req.url !== urlBeforeRewrite) { + debug('req.url has been rewritten from %s to %s', urlBeforeRewrite, + req.url); + } + } +} + +function escapeRegExp(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + /** * Check for an access token in cookies, headers, and query string parameters. * This function always checks for the following: @@ -39,19 +61,37 @@ module.exports = token; * @property {Array} [cookies] Array of cookie names. * @property {Array} [headers] Array of header names. * @property {Array} [params] Array of param names. - * @property {Array} [model] An AccessToken object to use. + * @property {Function|String} [model] AccessToken model name or class to use. + * @property {String} [currentUserLiteral] String literal for the current user. * @header loopback.token([options]) */ function token(options) { options = options || {}; var TokenModel = options.model || loopback.AccessToken; - assert(TokenModel, 'loopback.token() middleware requires a AccessToken model'); + if (typeof TokenModel === 'string') { + // Make it possible to configure the model in middleware.json + TokenModel = loopback.getModel(TokenModel); + } + var currentUserLiteral = options.currentUserLiteral; + if (currentUserLiteral && (typeof currentUserLiteral !== 'string')) { + debug('Set currentUserLiteral to \'me\' as the value is not a string.'); + currentUserLiteral = 'me'; + } + if (typeof currentUserLiteral === 'string') { + currentUserLiteral = escapeRegExp(currentUserLiteral); + } + assert(typeof TokenModel === 'function', + 'loopback.token() middleware requires a AccessToken model'); return function(req, res, next) { - if (req.accessToken !== undefined) return next(); + if (req.accessToken !== undefined) { + rewriteUserLiteral(req, currentUserLiteral); + return next(); + } TokenModel.findForRequest(req, options, function(err, token) { req.accessToken = token || null; + rewriteUserLiteral(req, currentUserLiteral); var ctx = loopback.getCurrentContext(); if (ctx) ctx.set('accessToken', token); next(err); diff --git a/test/access-token.test.js b/test/access-token.test.js index 58649554..726e1516 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -108,6 +108,51 @@ describe('loopback.token(options)', function() { }); }); + it('should rewrite url for the current user literal at the end without query', + function(done) { + var app = createTestApp(this.token, done); + var id = this.token.id; + var userId = this.token.userId; + request(app) + .get('/users/me') + .set('authorization', id) + .end(function(err, res) { + assert(!err); + assert.deepEqual(res.body, {userId: userId}); + done(); + }); + }); + + it('should rewrite url for the current user literal at the end with query', + function(done) { + var app = createTestApp(this.token, done); + var id = this.token.id; + var userId = this.token.userId; + request(app) + .get('/users/me?state=1') + .set('authorization', id) + .end(function(err, res) { + assert(!err); + assert.deepEqual(res.body, {userId: userId, state: 1}); + done(); + }); + }); + + it('should rewrite url for the current user literal in the middle', + function(done) { + var app = createTestApp(this.token, done); + var id = this.token.id; + var userId = this.token.userId; + request(app) + .get('/users/me/1') + .set('authorization', id) + .end(function(err, res) { + assert(!err); + assert.deepEqual(res.body, {userId: userId, state: 1}); + done(); + }); + }); + it('should skip when req.token is already present', function(done) { var tokenStub = { id: 'stub id' }; app.use(function(req, res, next) { @@ -284,7 +329,7 @@ describe('app.enableAuth()', function() { function createTestingToken(done) { var test = this; - Token.create({}, function(err, token) { + Token.create({userId: '123'}, function(err, token) { if (err) return done(err); test.token = token; done(); @@ -307,7 +352,7 @@ function createTestApp(testToken, settings, done) { var app = loopback(); app.use(loopback.cookieParser('secret')); - app.use(loopback.token({model: Token})); + app.use(loopback.token({model: 'MyToken', currentUserLiteral: 'me'})); app.get('/token', function(req, res) { res.cookie('authorization', testToken.id, {signed: true}); res.end(); @@ -321,6 +366,15 @@ function createTestApp(testToken, settings, done) { } res.send('ok'); }); + app.use('/users/:uid', function(req, res) { + var result = {userId: req.params.uid}; + if (req.query.state) { + result.state = req.query.state; + } else if (req.url !== '/') { + result.state = req.url.substring(1); + } + res.status(200).send(result); + }); app.use(loopback.rest()); app.enableAuth();