Files
cloudron-box/src/accesscontrol.js

148 lines
5.5 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
SCOPE_APPS_READ: 'apps:read',
SCOPE_APPS_MANAGE: 'apps:manage',
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_USERS_READ: 'users:read',
SCOPE_USERS_MANAGE: 'users:manage',
SCOPE_APPSTORE: 'appstore',
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
SCOPE_ANY: '*',
2018-06-17 22:29:17 -07:00
validateScopeString: validateScopeString,
2018-06-14 16:32:24 -07:00
hasScopes: hasScopes,
canonicalScopeString: canonicalScopeString,
2018-06-17 22:38:14 -07:00
intersectScopes: intersectScopes,
validateToken: validateToken,
2018-07-26 10:20:19 -07:00
scopesForUser: scopesForUser
2018-06-18 14:21:54 -07:00
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
settings = require('./settings.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
_ = 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(',');
}
2018-06-17 22:38:14 -07:00
function intersectScopes(allowedScopes, wantedScopes) {
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
2018-05-01 13:34:46 -07:00
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
2018-06-27 18:08:38 -07:00
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;
2018-05-01 13:34:46 -07:00
}
2018-06-17 22:29:17 -07:00
function validateScopeString(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new Error('Empty scope not allowed');
// 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 Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
return null;
}
2018-06-14 16:32:24 -07:00
// tests if all requiredScopes are attached to the request
2018-06-17 19:54:05 -07:00
function hasScopes(authorizedScopes, requiredScopes) {
assert(Array.isArray(authorizedScopes), 'Expecting array');
2018-06-14 16:32:24 -07:00
assert(Array.isArray(requiredScopes), 'Expecting array');
2018-06-17 19:54:05 -07:00
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
2018-06-14 16:32:24 -07:00
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
2018-06-17 19:54:05 -07:00
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
2018-06-14 16:32:24 -07:00
debug('scope: missing scope "%s".', requiredScopes[i]);
return new Error('Missing required scope "' + requiredScopes[i] + '"');
}
}
return null;
}
2018-06-18 14:21:54 -07:00
2018-08-02 19:07:33 -07:00
function scopesForUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (user.admin) return callback(null, exports.VALID_SCOPES);
settings.getSpacesConfig(function (error, spaces) {
if (error) return callback(error);
callback(null, spaces.enabled ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
});
}
function validateToken(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.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
2018-07-26 10:20:19 -07:00
users.get(token.identifier, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
2018-08-02 19:07:33 -07:00
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
2018-08-02 19:07:33 -07:00
callback(null, user, info);
});
});
});
2018-06-18 14:21:54 -07:00
}