'use strict'; exports = module.exports = { SCOPE_APPS_READ: 'apps:read', SCOPE_APPS_MANAGE: 'apps:manage', SCOPE_APPSTORE: 'appstore', SCOPE_CLIENTS: 'clients', SCOPE_CLOUDRON: 'cloudron', SCOPE_DOMAINS_READ: 'domains:read', SCOPE_DOMAINS_MANAGE: 'domains:manage', SCOPE_MAIL: 'mail', SCOPE_PROFILE: 'profile', SCOPE_SETTINGS: 'settings', SCOPE_SUBSCRIPTION: 'subscription', SCOPE_USERS_READ: 'users:read', SCOPE_USERS_MANAGE: 'users:manage', VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'subscription', 'users' ], // keep this sorted SCOPE_ANY: '*', validateScopeString: validateScopeString, hasScopes: hasScopes, canonicalScopeString: canonicalScopeString, intersectScopes: intersectScopes, validateToken: validateToken, scopesForUser: scopesForUser }; var assert = require('assert'), BoxError = require('./boxerror.js'), debug = require('debug')('box:accesscontrol'), tokendb = require('./tokendb.js'), users = require('./users.js'), _ = require('underscore'); // returns scopes that does not have wildcards and is sorted function canonicalScopeString(scope) { if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(','); return scope.split(',').sort().join(','); } function intersectScopes(allowedScopes, wantedScopes) { assert(Array.isArray(allowedScopes), 'Expecting sorted array'); assert(Array.isArray(wantedScopes), 'Expecting sorted array'); if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path let wantedScopesMap = new Map(); let results = []; // make a map of scope -> [ subscopes ] for (let w of wantedScopes) { let parts = w.split(':'); let subscopes = wantedScopesMap.get(parts[0]) || new Set(); subscopes.add(parts[1] || '*'); wantedScopesMap.set(parts[0], subscopes); } for (let a of allowedScopes) { let parts = a.split(':'); let as = parts[1] || '*'; let subscopes = wantedScopesMap.get(parts[0]); if (!subscopes) continue; if (subscopes.has('*') || subscopes.has(as)) { results.push(a); } else if (as === '*') { results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; })); } } return results; } function validateScopeString(scope) { assert.strictEqual(typeof scope, 'string'); if (scope === '') return new BoxError(BoxError.BAD_FIELD, 'Empty scope not allowed', { field: 'scope' }); // NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow // us not write a migration script every time we add a new scope var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; }); if (!allValid) return new BoxError(BoxError.BAD_FIELD, 'Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '), { field: 'scope' }); return null; } // tests if all requiredScopes are attached to the request function hasScopes(authorizedScopes, requiredScopes) { assert(Array.isArray(authorizedScopes), 'Expecting array'); assert(Array.isArray(requiredScopes), 'Expecting array'); if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null; for (var i = 0; i < requiredScopes.length; ++i) { const scopeParts = requiredScopes[i].split(':'); // this allows apps:write if the token has a higher apps scope if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) { debug('scope: missing scope "%s".', requiredScopes[i]); return new BoxError(BoxError.NOT_FOUND, 'Missing required scope "' + requiredScopes[i] + '"'); } } return null; } function scopesForUser(user, callback) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof callback, 'function'); if (user.admin) return callback(null, exports.VALID_SCOPES); callback(null, [ 'profile', 'apps:read' ]); } function validateToken(accessToken, callback) { assert.strictEqual(typeof accessToken, 'string'); assert.strictEqual(typeof callback, 'function'); tokendb.getByAccessToken(accessToken, function (error, token) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401 if (error) return callback(error); // this triggers 'internal error' in passport users.get(token.identifier, function (error, user) { if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401 if (error) return callback(error); if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401 scopesForUser(user, function (error, userScopes) { if (error) return callback(error); const authorizedScopes = intersectScopes(userScopes, token.scope.split(',')); callback(null, user, { authorizedScopes }); // ends up in req.authInfo }); }); }); }