npm WARN deprecated ejs-cli@1.2.0: This has breaking change. (in ejs package) use <= 2.0.0. npm WARN deprecated node-uuid@1.4.8: Use uuid module instead npm WARN deprecated minimatch@0.3.0: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue npm WARN deprecated minimatch@2.0.10: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue npm WARN deprecated minimatch@0.2.14: Please update to minimatch 3.0.2 or higher to avoid a RegExp DoS issue npm WARN deprecated graceful-fs@1.2.3: graceful-fs v3.0.0 and before will fail on node releases >= v7.0. Please update to graceful-fs@^4.0.0 as soon as possible. Use 'npm ls graceful-fs' to find it in the tree.
337 lines
12 KiB
JavaScript
337 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
ClientsError: ClientsError,
|
|
|
|
add: add,
|
|
get: get,
|
|
del: del,
|
|
getAll: getAll,
|
|
getByAppIdAndType: getByAppIdAndType,
|
|
getClientTokensByUserId: getClientTokensByUserId,
|
|
delClientTokensByUserId: delClientTokensByUserId,
|
|
delByAppIdAndType: delByAppIdAndType,
|
|
addClientTokenByUserId: addClientTokenByUserId,
|
|
delToken: delToken,
|
|
|
|
addDefaultClients: addDefaultClients,
|
|
|
|
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
|
|
SCOPE_APPS: 'apps',
|
|
SCOPE_DEVELOPER: 'developer',
|
|
SCOPE_PROFILE: 'profile',
|
|
SCOPE_CLOUDRON: 'cloudron',
|
|
SCOPE_SETTINGS: 'settings',
|
|
SCOPE_USERS: 'users',
|
|
|
|
// roles are handled just like the above scopes, they are parallel to scopes
|
|
// scopes enclose API groups, roles specify the usage role
|
|
SCOPE_ROLE_SDK: 'roleSdk',
|
|
|
|
// client type enums
|
|
TYPE_EXTERNAL: 'external',
|
|
TYPE_BUILT_IN: 'built-in',
|
|
TYPE_OAUTH: 'addon-oauth',
|
|
TYPE_PROXY: 'addon-proxy'
|
|
};
|
|
|
|
var appdb = require('./appdb.js'),
|
|
assert = require('assert'),
|
|
async = require('async'),
|
|
clientdb = require('./clientdb.js'),
|
|
config = require('./config.js'),
|
|
DatabaseError = require('./databaseerror.js'),
|
|
debug = require('debug')('box:clients'),
|
|
hat = require('hat'),
|
|
tokendb = require('./tokendb.js'),
|
|
util = require('util'),
|
|
uuid = require('uuid');
|
|
|
|
function ClientsError(reason, errorOrMessage) {
|
|
assert.strictEqual(typeof reason, 'string');
|
|
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
|
|
|
Error.call(this);
|
|
Error.captureStackTrace(this, this.constructor);
|
|
|
|
this.name = this.constructor.name;
|
|
this.reason = reason;
|
|
if (typeof errorOrMessage === 'undefined') {
|
|
this.message = reason;
|
|
} else if (typeof errorOrMessage === 'string') {
|
|
this.message = errorOrMessage;
|
|
} else {
|
|
this.message = 'Internal error';
|
|
this.nestedError = errorOrMessage;
|
|
}
|
|
}
|
|
util.inherits(ClientsError, Error);
|
|
ClientsError.INVALID_SCOPE = 'Invalid scope';
|
|
ClientsError.INVALID_CLIENT = 'Invalid client';
|
|
ClientsError.INVALID_TOKEN = 'Invalid token';
|
|
ClientsError.BAD_FIELD = 'Bad field';
|
|
ClientsError.NOT_FOUND = 'Not found';
|
|
ClientsError.INTERNAL_ERROR = 'Internal Error';
|
|
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
|
|
|
|
function validateName(name) {
|
|
assert.strictEqual(typeof name, 'string');
|
|
|
|
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
|
|
if (name.length > 128) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
|
|
|
|
if (/[^a-zA-Z0-9\-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
|
|
|
|
return null;
|
|
}
|
|
|
|
function validateScope(scope) {
|
|
assert.strictEqual(typeof scope, 'string');
|
|
|
|
var VALID_SCOPES = [
|
|
exports.SCOPE_APPS,
|
|
exports.SCOPE_DEVELOPER,
|
|
exports.SCOPE_PROFILE,
|
|
exports.SCOPE_CLOUDRON,
|
|
exports.SCOPE_SETTINGS,
|
|
exports.SCOPE_USERS,
|
|
'*', // includes all scopes, but not roles
|
|
exports.SCOPE_ROLE_SDK
|
|
];
|
|
|
|
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
|
|
|
|
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
|
|
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
|
|
|
|
return null;
|
|
}
|
|
|
|
function add(appId, type, redirectURI, scope, callback) {
|
|
assert.strictEqual(typeof appId, 'string');
|
|
assert.strictEqual(typeof type, 'string');
|
|
assert.strictEqual(typeof redirectURI, 'string');
|
|
assert.strictEqual(typeof scope, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
// allow whitespace
|
|
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
|
|
|
|
var error = validateScope(scope);
|
|
if (error) return callback(error);
|
|
|
|
// appId is also client name
|
|
error = validateName(appId);
|
|
if (error) return callback(error);
|
|
|
|
var id = 'cid-' + uuid.v4();
|
|
var clientSecret = hat(8 * 128);
|
|
|
|
clientdb.add(id, appId, type, clientSecret, redirectURI, scope, function (error) {
|
|
if (error) return callback(error);
|
|
|
|
var client = {
|
|
id: id,
|
|
appId: appId,
|
|
type: type,
|
|
clientSecret: clientSecret,
|
|
redirectURI: redirectURI,
|
|
scope: scope
|
|
};
|
|
|
|
callback(null, client);
|
|
});
|
|
}
|
|
|
|
function get(id, callback) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
clientdb.get(id, function (error, result) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
|
if (error) return callback(error);
|
|
callback(null, result);
|
|
});
|
|
}
|
|
|
|
function del(id, callback) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
clientdb.del(id, function (error, result) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
|
if (error) return callback(error);
|
|
callback(null, result);
|
|
});
|
|
}
|
|
|
|
function getAll(callback) {
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
clientdb.getAll(function (error, results) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
|
|
if (error) return callback(error);
|
|
|
|
var tmp = [];
|
|
async.each(results, function (record, callback) {
|
|
if (record.type === exports.TYPE_EXTERNAL || record.type === exports.TYPE_BUILT_IN) {
|
|
// the appId in this case holds the name
|
|
record.name = record.appId;
|
|
|
|
tmp.push(record);
|
|
|
|
return callback(null);
|
|
}
|
|
|
|
appdb.get(record.appId, function (error, result) {
|
|
if (error) {
|
|
console.error('Failed to get app details for oauth client', record.appId, error);
|
|
return callback(null); // ignore error so we continue listing clients
|
|
}
|
|
|
|
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
|
|
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
|
|
|
|
record.location = result.location;
|
|
|
|
tmp.push(record);
|
|
|
|
callback(null);
|
|
});
|
|
}, function (error) {
|
|
if (error) return callback(error);
|
|
callback(null, tmp);
|
|
});
|
|
});
|
|
}
|
|
|
|
function getByAppIdAndType(appId, type, callback) {
|
|
assert.strictEqual(typeof appId, 'string');
|
|
assert.strictEqual(typeof type, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
clientdb.getByAppIdAndType(appId, type, function (error, result) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
|
if (error) return callback(error);
|
|
callback(null, result);
|
|
});
|
|
}
|
|
|
|
function getClientTokensByUserId(clientId, userId, callback) {
|
|
assert.strictEqual(typeof clientId, 'string');
|
|
assert.strictEqual(typeof userId, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
|
// this can mean either that there are no tokens or the clientId is actually unknown
|
|
get(clientId, function (error/*, result*/) {
|
|
if (error) return callback(error);
|
|
callback(null, []);
|
|
});
|
|
return;
|
|
}
|
|
if (error) return callback(error);
|
|
callback(null, result || []);
|
|
});
|
|
}
|
|
|
|
function delClientTokensByUserId(clientId, userId, callback) {
|
|
assert.strictEqual(typeof clientId, 'string');
|
|
assert.strictEqual(typeof userId, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
|
// this can mean either that there are no tokens or the clientId is actually unknown
|
|
get(clientId, function (error/*, result*/) {
|
|
if (error) return callback(error);
|
|
callback(null);
|
|
});
|
|
return;
|
|
}
|
|
if (error) return callback(error);
|
|
callback(null);
|
|
});
|
|
}
|
|
|
|
function delByAppIdAndType(appId, type, callback) {
|
|
assert.strictEqual(typeof appId, 'string');
|
|
assert.strictEqual(typeof type, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
getByAppIdAndType(appId, type, function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
tokendb.delByClientId(result.id, function (error) {
|
|
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
|
|
|
clientdb.delByAppIdAndType(appId, type, function (error) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
|
if (error) return callback(error);
|
|
|
|
callback(null);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
|
|
assert.strictEqual(typeof clientId, 'string');
|
|
assert.strictEqual(typeof userId, 'string');
|
|
assert.strictEqual(typeof expiresAt, 'number');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
get(clientId, function (error, result) {
|
|
if (error) return callback(error);
|
|
|
|
var token = tokendb.generateToken();
|
|
|
|
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
|
|
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
|
|
|
callback(null, {
|
|
accessToken: token,
|
|
identifier: userId,
|
|
clientId: result.id,
|
|
scope: result.id,
|
|
expires: expiresAt
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function delToken(clientId, tokenId, callback) {
|
|
assert.strictEqual(typeof clientId, 'string');
|
|
assert.strictEqual(typeof tokenId, 'string');
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
get(clientId, function (error) {
|
|
if (error) return callback(error);
|
|
|
|
tokendb.del(tokenId, function (error) {
|
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INVALID_TOKEN, 'Invalid token'));
|
|
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
|
|
|
callback(null);
|
|
});
|
|
});
|
|
}
|
|
|
|
function addDefaultClients(callback) {
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
debug('Adding default clients');
|
|
|
|
// The domain might have changed, therefor we have to update the record
|
|
// !!! This needs to be in sync with the webadmin, specifically login_callback.js
|
|
const ADMIN_SCOPES="cloudron,developer,profile,users,apps,settings";
|
|
|
|
// id, appId, type, clientSecret, redirectURI, scope
|
|
async.series([
|
|
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', config.adminOrigin(), ADMIN_SCOPES),
|
|
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', config.adminOrigin(), '*,roleSdk'),
|
|
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', config.adminOrigin(), '*, roleSdk')
|
|
], callback);
|
|
}
|