diff --git a/migrations/20200206154808-remove-clients.js b/migrations/20200206154808-remove-clients.js new file mode 100644 index 000000000..866346bab --- /dev/null +++ b/migrations/20200206154808-remove-clients.js @@ -0,0 +1,24 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('DROP TABLE clients', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + var cmd = `CREATE TABLE IF NOT EXISTS clients( + id VARCHAR(128) NOT NULL UNIQUE, + appId VARCHAR(128) NOT NULL, + type VARCHAR(16) NOT NULL, + clientSecret VARCHAR(512) NOT NULL, + redirectURI VARCHAR(512) NOT NULL, + scope VARCHAR(512) NOT NULL, + PRIMARY KEY(id)) CHARACTER SET utf8 COLLATE utf8_bin`; + + db.runSql(cmd, function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index c79948f6b..f3a2ab035 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -52,15 +52,6 @@ CREATE TABLE IF NOT EXISTS tokens( expires BIGINT NOT NULL, // FIXME: make this a timestamp PRIMARY KEY(accessToken)); -CREATE TABLE IF NOT EXISTS clients( - id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes - appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps) - type VARCHAR(16) NOT NULL, - clientSecret VARCHAR(512) NOT NULL, - redirectURI VARCHAR(512) NOT NULL, - scope VARCHAR(512) NOT NULL, - PRIMARY KEY(id)); - CREATE TABLE IF NOT EXISTS apps( id VARCHAR(128) NOT NULL UNIQUE, appStoreId VARCHAR(128) NOT NULL, // empty for custom apps diff --git a/package-lock.json b/package-lock.json index c5a9b7cdd..46e8dc302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4917,11 +4917,6 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, - "valid-url": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", - "integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=" - }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index 7214728f8..d9132ad13 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "tldjs": "^2.3.1", "underscore": "^1.9.2", "uuid": "^3.4.0", - "valid-url": "^1.0.9", "validator": "^11.0.0", "ws": "^7.2.1", "xml2js": "^0.4.23" diff --git a/src/addons.js b/src/addons.js index d8b0769cc..29c461629 100644 --- a/src/addons.js +++ b/src/addons.js @@ -29,13 +29,11 @@ exports = module.exports = { SERVICE_STATUS_STOPPED: 'stopped' }; -var accesscontrol = require('./accesscontrol.js'), - appdb = require('./appdb.js'), +var appdb = require('./appdb.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), BoxError = require('./boxerror.js'), - clients = require('./clients.js'), constants = require('./constants.js'), crypto = require('crypto'), debug = require('debug')('box:addons'), @@ -777,29 +775,11 @@ function setupOauth(app, options, callback) { if (!app.sso) return callback(null); - var appId = app.id; - var redirectURI = 'https://' + app.fqdn; - var scope = accesscontrol.SCOPE_PROFILE; + const env = []; - clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds - if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); + debugApp(app, 'Setting oauth addon config to %j', env); - clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) { - if (error) return callback(error); - - const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_'; - - var env = [ - { name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id }, - { name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret }, - { name: `${envPrefix}OAUTH_ORIGIN`, value: settings.adminOrigin() } - ]; - - debugApp(app, 'Setting oauth addon config to %j', env); - - appdb.setAddonConfig(appId, 'oauth', env, callback); - }); - }); + appdb.setAddonConfig(app.id, 'oauth', env, callback); } function teardownOauth(app, options, callback) { @@ -809,11 +789,7 @@ function teardownOauth(app, options, callback) { debugApp(app, 'teardownOauth'); - clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) { - if (error && error.reason !== BoxError.NOT_FOUND) debug(error); - - appdb.unsetAddonConfig(app.id, 'oauth', callback); - }); + appdb.unsetAddonConfig(app.id, 'oauth', callback); } function setupEmail(app, options, callback) { diff --git a/src/clientdb.js b/src/clientdb.js deleted file mode 100644 index 81bb72a63..000000000 --- a/src/clientdb.js +++ /dev/null @@ -1,179 +0,0 @@ -'use strict'; - -exports = module.exports = { - get: get, - getAll: getAll, - getAllWithTokenCount: getAllWithTokenCount, - getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier, - add: add, - del: del, - getByAppId: getByAppId, - getByAppIdAndType: getByAppIdAndType, - - upsert: upsert, - - delByAppId: delByAppId, - delByAppIdAndType: delByAppIdAndType, - - _clear: clear -}; - -var assert = require('assert'), - BoxError = require('./boxerror.js'), - database = require('./database.js'); - -var CLIENTS_FIELDS = [ 'id', 'appId', 'type', 'clientSecret', 'redirectURI', 'scope' ].join(','); -var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.type', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(','); - -function get(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, `Client not found: ${id}`)); - - callback(null, result[0]); - }); -} - -function getAll(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients ORDER BY appId', function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function getAllWithTokenCount(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId GROUP BY clients.id', [], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function getAllWithTokenCountByIdentifier(identifier, callback) { - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null, results); - }); -} - -function getByAppId(appId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? LIMIT 1', [ appId ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found')); - - callback(null, result[0]); - }); -} - -function getByAppIdAndType(appId, type, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? AND type = ? LIMIT 1', [ appId, type ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found')); - - callback(null, result[0]); - }); -} - -function add(id, appId, type, clientSecret, redirectURI, scope, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof clientSecret, 'string'); - assert.strictEqual(typeof redirectURI, 'string'); - assert.strictEqual(typeof scope, 'string'); - assert.strictEqual(typeof callback, 'function'); - - var data = [ id, appId, type, clientSecret, redirectURI, scope ]; - - database.query('INSERT INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS)); - if (error || result.affectedRows === 0) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function upsert(id, appId, type, clientSecret, redirectURI, scope, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof clientSecret, 'string'); - assert.strictEqual(typeof redirectURI, 'string'); - assert.strictEqual(typeof scope, 'string'); - assert.strictEqual(typeof callback, 'function'); - - var data = [ id, appId, type, clientSecret, redirectURI, scope ]; - - database.query('REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) { - if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS)); - if (error || result.affectedRows === 0) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function del(id, callback) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM clients WHERE id = ?', [ id ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, `Client not found: ${id}`)); - - callback(null); - }); -} - -function delByAppId(appId, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM clients WHERE appId=?', [ appId ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found')); - - callback(null); - }); -} - -function delByAppIdAndType(appId, type, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof type, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM clients WHERE appId=? AND type=?', [ appId, type ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found')); - - callback(null); - }); -} - -function clear(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('DELETE FROM clients', function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} diff --git a/src/clients.js b/src/clients.js deleted file mode 100644 index d5c9cfd6c..000000000 --- a/src/clients.js +++ /dev/null @@ -1,329 +0,0 @@ -'use strict'; - -exports = module.exports = { - add: add, - get: get, - del: del, - getAll: getAll, - getByAppIdAndType: getByAppIdAndType, - getTokensByUserId: getTokensByUserId, - delTokensByUserId: delTokensByUserId, - delByAppIdAndType: delByAppIdAndType, - addTokenByUserId: addTokenByUserId, - delToken: delToken, - - issueDeveloperToken: issueDeveloperToken, - - addDefaultClients: addDefaultClients, - - removeTokenPrivateFields: removeTokenPrivateFields, - - // client ids. we categorize them so we can have different restrictions based on the client - ID_WEBADMIN: 'cid-webadmin', // dashboard oauth - ID_SDK: 'cid-sdk', // created by user via dashboard - ID_CLI: 'cid-cli', // created via cli tool - - // client type enums - TYPE_EXTERNAL: 'external', - TYPE_BUILT_IN: 'built-in', - TYPE_OAUTH: 'addon-oauth', - TYPE_PROXY: 'addon-proxy' -}; - -var apps = require('./apps.js'), - assert = require('assert'), - async = require('async'), - BoxError = require('./boxerror.js'), - clientdb = require('./clientdb.js'), - constants = require('./constants.js'), - debug = require('debug')('box:clients'), - eventlog = require('./eventlog.js'), - hat = require('./hat.js'), - accesscontrol = require('./accesscontrol.js'), - tokendb = require('./tokendb.js'), - users = require('./users.js'), - uuid = require('uuid'), - _ = require('underscore'); - -function validateClientName(name) { - assert.strictEqual(typeof name, 'string'); - - if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 character', { field: 'name' }); - if (name.length > 128) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' }); - - if (/[^a-zA-Z0-9-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'name can only contain alphanumerals and dash', { field: 'name' }); - - return null; -} - -function validateTokenName(name) { - assert.strictEqual(typeof name, 'string'); - - if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' }); - - 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'); - - var error = accesscontrol.validateScopeString(scope); - if (error) return callback(error); - - error = validateClientName(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) 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) return callback(error); - - callback(null, result); - }); -} - -function getAll(callback) { - assert.strictEqual(typeof callback, 'function'); - - clientdb.getAll(function (error, results) { - if (error && error.reason === BoxError.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); - } - - apps.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.domain = result.fqdn; - - 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) return callback(error); - - callback(null, result); - }); -} - -function getTokensByUserId(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 === BoxError.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 delTokensByUserId(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 === BoxError.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 !== BoxError.NOT_FOUND) return callback(error); - - clientdb.delByAppIdAndType(appId, type, function (error) { - if (error) return callback(error); - - callback(null); - }); - }); - }); -} - -function addTokenByUserId(clientId, userId, expiresAt, options, callback) { - assert.strictEqual(typeof clientId, 'string'); - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof expiresAt, 'number'); - assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const name = options.name || ''; - let error = validateTokenName(name); - if (error) return callback(error); - - get(clientId, function (error, result) { - if (error) return callback(error); - - users.get(userId, function (error, user) { - if (error) return callback(error); - - accesscontrol.scopesForUser(user, function (error, userScopes) { - if (error) return callback(error); - - const scope = accesscontrol.canonicalScopeString(result.scope); - const authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(',')); - - const token = { - id: 'tid-' + uuid.v4(), - accessToken: hat(8 * 32), - identifier: userId, - clientId: result.id, - expires: expiresAt, - scope: authorizedScopes.join(','), - name: name - }; - - tokendb.add(token, function (error) { - if (error) return callback(error); - - callback(null, { - accessToken: token.accessToken, - tokenScopes: authorizedScopes, - identifier: userId, - clientId: result.id, - expires: expiresAt - }); - }); - }); - }); - }); -} - -function issueDeveloperToken(userObject, auditSource, callback) { - assert.strictEqual(typeof userObject, 'object'); - assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - - const expiresAt = Date.now() + (30 * 24 * 60 * 60 * 1000); // cli tokens are valid for a month - - addTokenByUserId(exports.ID_CLI, userObject.id, expiresAt, {}, function (error, result) { - if (error) return callback(error); - - eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) }); - - callback(null, result); - }); -} - -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) return callback(error); - - callback(null); - }); - }); -} - -function addDefaultClients(origin, callback) { - assert.strictEqual(typeof origin, 'string'); - assert.strictEqual(typeof callback, 'function'); - - debug('Adding default clients'); - - // The domain might have changed, therefor we have to update the record - // id, appId, type, clientSecret, redirectURI, scope - async.series([ - clientdb.upsert.bind(null, exports.ID_WEBADMIN, 'Settings', 'built-in', 'secret-webadmin', origin, '*'), - clientdb.upsert.bind(null, exports.ID_SDK, 'SDK', 'built-in', 'secret-sdk', origin, '*'), - clientdb.upsert.bind(null, exports.ID_CLI, 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*') - ], callback); -} - -function removeTokenPrivateFields(token) { - return _.pick(token, 'id', 'identifier', 'clientId', 'scope', 'expires', 'name'); -} diff --git a/src/cloudron.js b/src/cloudron.js index 1b27240bd..68f079b7a 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -27,7 +27,6 @@ var apps = require('./apps.js'), auditSource = require('./auditsource.js'), backups = require('./backups.js'), BoxError = require('./boxerror.js'), - clients = require('./clients.js'), constants = require('./constants.js'), cron = require('./cron.js'), debug = require('debug')('box:cloudron'), @@ -288,10 +287,7 @@ function setDashboardDomain(domain, auditSource, callback) { const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject); - async.series([ - (done) => settings.setAdmin(domain, fqdn, done), - (done) => clients.addDefaultClients(settings.adminOrigin(), done) - ], function (error) { + settings.setAdmin(domain, fqdn, function (error) { if (error) return callback(error); eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn }); diff --git a/src/database.js b/src/database.js index f79d73ab0..d1d1c14c1 100644 --- a/src/database.js +++ b/src/database.js @@ -104,10 +104,7 @@ function clear(callback) { gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name, gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name); - async.series([ - child_process.exec.bind(null, cmd), - require('./clients.js').addDefaultClients.bind(null, 'https://admin-localhost') - ], callback); + child_process.exec(cmd, callback); } function beginTransaction(callback) { diff --git a/src/provision.js b/src/provision.js index d5cd26f77..857bf05dc 100644 --- a/src/provision.js +++ b/src/provision.js @@ -15,7 +15,6 @@ var appstore = require('./appstore.js'), backups = require('./backups.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), - clients = require('./clients.js'), cloudron = require('./cloudron.js'), debug = require('debug')('box:provision'), domains = require('./domains.js'), @@ -29,6 +28,7 @@ var appstore = require('./appstore.js'), sysinfo = require('./sysinfo.js'), users = require('./users.js'), tld = require('tldjs'), + tokens = require('./tokens.js'), _ = require('underscore'); const NOOP_CALLBACK = function (error) { if (error) debug(error); }; @@ -165,7 +165,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated')); if (error) return callback(error); - clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { + tokens.addTokenByUserId(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { if (error) return callback(error); eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { }); diff --git a/src/routes/clients.js b/src/routes/clients.js deleted file mode 100644 index 1bbe23254..000000000 --- a/src/routes/clients.js +++ /dev/null @@ -1,124 +0,0 @@ -'use strict'; - -exports = module.exports = { - add: add, - get: get, - del: del, - getAll: getAll, - addToken: addToken, - getTokens: getTokens, - delTokens: delTokens, - delToken: delToken -}; - -var assert = require('assert'), - BoxError = require('../boxerror.js'), - clients = require('../clients.js'), - constants = require('../constants.js'), - HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess, - validUrl = require('valid-url'); - -function add(req, res, next) { - var data = req.body; - - if (!data) return next(new HttpError(400, 'Cannot parse data field')); - if (typeof data.appId !== 'string' || !data.appId) return next(new HttpError(400, 'appId is required')); - if (typeof data.redirectURI !== 'string' || !data.redirectURI) return next(new HttpError(400, 'redirectURI is required')); - if (typeof data.scope !== 'string' || !data.scope) return next(new HttpError(400, 'scope is required')); - if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri')); - - clients.add(data.appId, clients.TYPE_EXTERNAL, data.redirectURI, data.scope, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(201, result)); - }); -} - -function get(req, res, next) { - assert.strictEqual(typeof req.params.clientId, 'string'); - - clients.get(req.params.clientId, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, result)); - }); -} - -function del(req, res, next) { - assert.strictEqual(typeof req.params.clientId, 'string'); - - clients.get(req.params.clientId, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - // we do not allow to use the REST API to delete addon clients - if (result.type !== clients.TYPE_EXTERNAL) return next(new HttpError(405, 'Deleting app addon clients is not allowed.')); - - clients.del(req.params.clientId, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(204, result)); - }); - }); -} - -function getAll(req, res, next) { - clients.getAll(function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, { clients: result })); - }); -} - -function addToken(req, res, next) { - assert.strictEqual(typeof req.params.clientId, 'string'); - assert.strictEqual(typeof req.user, 'object'); - assert.strictEqual(typeof req.body, 'object'); - - var data = req.body; - var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION; - if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future')); - if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string')); - - clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(201, { token: result })); - }); -} - -function getTokens(req, res, next) { - assert.strictEqual(typeof req.params.clientId, 'string'); - assert.strictEqual(typeof req.user, 'object'); - - clients.getTokensByUserId(req.params.clientId, req.user.id, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); - - result = result.map(clients.removeTokenPrivateFields); - - next(new HttpSuccess(200, { tokens: result })); - }); -} - -function delTokens(req, res, next) { - assert.strictEqual(typeof req.params.clientId, 'string'); - assert.strictEqual(typeof req.user, 'object'); - - clients.delTokensByUserId(req.params.clientId, req.user.id, function (error) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(204)); - }); -} - -function delToken(req, res, next) { - assert.strictEqual(typeof req.params.clientId, 'string'); - assert.strictEqual(typeof req.params.tokenId, 'string'); - assert.strictEqual(typeof req.user, 'object'); - - clients.delToken(req.params.clientId, req.params.tokenId, function (error) { - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(204)); - }); -} diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index c584b34dd..f0d8f9d17 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -27,7 +27,6 @@ let assert = require('assert'), async = require('async'), auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), - clients = require('../clients.js'), cloudron = require('../cloudron.js'), constants = require('../constants.js'), debug = require('debug')('box:routes/cloudron'), @@ -38,6 +37,7 @@ let assert = require('assert'), sysinfo = require('../sysinfo.js'), system = require('../system.js'), tokendb = require('../tokendb.js'), + tokens = require('../tokens.js'), updater = require('../updater.js'), users = require('../users.js'), updateChecker = require('../updatechecker.js'); @@ -48,7 +48,7 @@ function login(req, res, next) { const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; const auditSource = { authType: 'basic', ip: ip }; - clients.addTokenByUserId(clients.ID_WEBADMIN, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { + tokens.addTokenByUserId(tokens.ID_WEBADMIN, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { if (error) return next(new HttpError(500, error)); eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); @@ -104,7 +104,7 @@ function passwordReset(req, res, next) { if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(406, error.message)); if (error) return next(new HttpError(500, error)); - clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { + tokens.addTokenByUserId(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { if (error) return next(new HttpError(500, error)); next(new HttpSuccess(202, { accessToken: result.accessToken })); @@ -141,7 +141,7 @@ function setupAccount(req, res, next) { if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); if (error) return next(new HttpError(500, error)); - clients.addTokenByUserId(clients.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { + tokens.addTokenByUserId(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) { if (error) return next(new HttpError(500, error)); next(new HttpSuccess(201, { accessToken: result.accessToken })); diff --git a/src/routes/index.js b/src/routes/index.js index cc506f5bb..f2f7bdc67 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -6,7 +6,6 @@ exports = module.exports = { apps: require('./apps.js'), appstore: require('./appstore.js'), backups: require('./backups.js'), - clients: require('./clients.js'), cloudron: require('./cloudron.js'), domains: require('./domains.js'), eventlog: require('./eventlog.js'), diff --git a/src/routes/test/apps-test.js b/src/routes/test/apps-test.js index dace5740e..33afdb285 100644 --- a/src/routes/test/apps-test.js +++ b/src/routes/test/apps-test.js @@ -7,7 +7,6 @@ let apps = require('../../apps.js'), async = require('async'), child_process = require('child_process'), - clients = require('../../clients.js'), constants = require('../../constants.js'), crypto = require('crypto'), database = require('../../database.js'), @@ -29,6 +28,7 @@ let apps = require('../../apps.js'), settingsdb = require('../../settingsdb.js'), superagent = require('superagent'), tokendb = require('../../tokendb.js'), + tokens = require('../../tokens.js'), url = require('url'); var SERVER_URL = 'http://localhost:' + constants.PORT; @@ -213,7 +213,7 @@ function startBox(done) { token_1 = hat(8 * 32); // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: user_1_id, clientId: clients.ID_SDK, expires: Date.now() + 1000000, scope: 'apps', name: '' }, callback); + tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: user_1_id, clientId: tokens.ID_SDK, expires: Date.now() + 1000000, scope: 'apps', name: '' }, callback); }); }, @@ -633,22 +633,6 @@ describe('App API', function () { }); }); - it('oauth addon config', function (done) { - var appContainer = docker.getContainer(appEntry.containerId); - appContainer.inspect(function (error, data) { - expect(error).to.not.be.ok(); - - clients.getByAppIdAndType(APP_ID, clients.TYPE_OAUTH, function (error, client) { - expect(error).to.not.be.ok(); - expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens - expect(client.clientSecret.length).to.be(256); // 32 hex chars (8 * 256 bits) - expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_ID=' + client.id); - expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_SECRET=' + client.clientSecret); - done(); - }); - }); - }); - it('installation - app can populate addons', function (done) { superagent.get(`http://localhost:${appEntry.httpPort}/populate_addons`).end(function (error, res) { expect(!error).to.be.ok(); diff --git a/src/routes/test/clients-test.js b/src/routes/test/clients-test.js deleted file mode 100644 index e855b11ca..000000000 --- a/src/routes/test/clients-test.js +++ /dev/null @@ -1,537 +0,0 @@ -'use strict'; - -/* global it:false */ -/* global describe:false */ -/* global before:false */ -/* global after:false */ - -var accesscontrol = require('../../accesscontrol.js'), - async = require('async'), - constants = require('../../constants.js'), - clients = require('../../clients.js'), - database = require('../../database.js'), - expect = require('expect.js'), - uuid = require('uuid'), - hat = require('../../hat.js'), - superagent = require('superagent'), - server = require('../../server.js'); - -var SERVER_URL = 'http://localhost:' + constants.PORT; - -var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com'; -var token = null; - -function setup(done) { - async.series([ - server.start, - database._clear, - - function (callback) { - superagent.post(SERVER_URL + '/api/v1/cloudron/activate') - .query({ setupToken: 'somesetuptoken' }) - .send({ username: USERNAME, password: PASSWORD, email: EMAIL }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.statusCode).to.equal(201); - - // stash token for further use - token = result.body.token; - - callback(); - }); - } - ], done); -} - -function cleanup(done) { - database._clear(function (error) { - expect(error).to.not.be.ok(); - - server.stop(done); - }); -} - -describe('OAuth Clients API', function () { - describe('add', function () { - before(setup), - after(cleanup); - - it('fails without token', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails without appId', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails with empty appId', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: '', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails without scope', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'http://foobar.com' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails with empty scope', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails without redirectURI', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails with empty redirectURI', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: '', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails with malformed redirectURI', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'foobar', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('fails with invalid name', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(400); - done(); - }); - }); - - it('succeeds with dash', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - expect(result.body.id).to.be.a('string'); - expect(result.body.appId).to.be.a('string'); - expect(result.body.redirectURI).to.be.a('string'); - expect(result.body.clientSecret).to.be.a('string'); - expect(result.body.scope).to.be.a('string'); - expect(result.body.type).to.equal(clients.TYPE_EXTERNAL); - - done(); - }); - }); - }); - - describe('get', function () { - var CLIENT_0 = { - id: '', - appId: 'someAppId-0', - redirectURI: 'http://some.callback0', - scope: accesscontrol.SCOPE_PROFILE - }; - - before(function (done) { - async.series([ - setup, - - function (callback) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - - CLIENT_0 = result.body; - - callback(); - }); - } - ], done); - }); - - after(cleanup); - - it('fails without token', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - - it('fails with unknown id', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id.toUpperCase()) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - expect(result.body).to.eql(CLIENT_0); - done(); - }); - }); - }); - - describe('del', function () { - var CLIENT_0 = { - id: '', - appId: 'someAppId-0', - redirectURI: 'http://some.callback0', - scope: accesscontrol.SCOPE_PROFILE - }; - - var CLIENT_1 = { - id: '', - appId: 'someAppId-1', - redirectURI: 'http://some.callback1', - scope: accesscontrol.SCOPE_PROFILE, - type: clients.TYPE_OAUTH - }; - - before(function (done) { - async.series([ - setup, - - function (callback) { - superagent.post(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope }) - .end(function (error, result) { - expect(result.statusCode).to.equal(201); - - CLIENT_0 = result.body; - - callback(); - }); - } - ], done); - }); - - after(cleanup); - - it('fails without token', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - - it('fails with unknown id', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id.toUpperCase()) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - - superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - - done(); - }); - }); - }); - - it('fails for cid-webadmin', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(405); - - superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - done(); - }); - }); - }); - - it('fails for addon auth client', function (done) { - clients.add(CLIENT_1.appId, CLIENT_1.type, CLIENT_1.redirectURI, CLIENT_1.scope, function (error, result) { - expect(error).to.equal(null); - - CLIENT_1.id = result.id; - - superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_1.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(405); - - superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_1.id) - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - done(); - }); - }); - }); - }); - }); -}); - -describe('Clients', function () { - var USER_0 = { - userId: uuid.v4(), - username: 'someusername', - password: 'Strong#$%2345', - email: 'some@email.com', - admin: true, - salt: 'somesalt', - createdAt: (new Date()).toISOString(), - modifiedAt: (new Date()).toISOString(), - resetToken: hat(256) - }; - - function setup2(done) { - async.series([ - setup, - - function (callback) { - superagent.get(SERVER_URL + '/api/v1/profile') - .query({ access_token: token }) - .end(function (error, result) { - expect(result).to.be.ok(); - expect(result.statusCode).to.eql(200); - - USER_0.id = result.body.id; - - callback(); - }); - } - ], done); - } - - describe('get', function () { - before(setup2); - after(cleanup); - - it('fails due to missing token', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients') - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to empty token', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients') - .query({ access_token: '' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to wrong token', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients') - .query({ access_token: token.toUpperCase() }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - expect(result.body.clients.length).to.eql(3); - - done(); - }); - }); - }); - - describe('get tokens by client', function () { - before(setup2); - after(cleanup); - - it('fails due to missing token', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to empty token', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .query({ access_token: '' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to wrong token', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .query({ access_token: token.toUpperCase() }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to unkown client', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/CID-WEBADMIN/tokens') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - expect(result.body.tokens.length).to.eql(1); - expect(result.body.tokens[0].identifier).to.eql(USER_0.id); - - done(); - }); - }); - }); - - describe('delete tokens by client', function () { - before(setup2); - after(cleanup); - - it('fails due to missing token', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to empty token', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .query({ access_token: '' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to wrong token', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .query({ access_token: token.toUpperCase() }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - - it('fails due to unkown client', function (done) { - superagent.del(SERVER_URL + '/api/v1/clients/CID-WEBADMIN/tokens') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(404); - done(); - }); - }); - - it('succeeds', function (done) { - superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); - - expect(result.body.tokens.length).to.eql(1); - expect(result.body.tokens[0].identifier).to.eql(USER_0.id); - - superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(204); - - // further calls with this token should not work - superagent.get(SERVER_URL + '/api/v1/profile') - .query({ access_token: token }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); - done(); - }); - }); - }); - }); - }); -}); diff --git a/src/server.js b/src/server.js index 24d4f8208..b30f21571 100644 --- a/src/server.js +++ b/src/server.js @@ -176,17 +176,6 @@ function initializeExpressSync() { router.post('/api/v1/groups/:groupId', token, authorizeAdmin, routes.groups.update); router.del ('/api/v1/groups/:groupId', token, authorizeAdmin, routes.groups.remove); - // client/token routes - router.get ('/api/v1/clients', token, authorizeAdmin, routes.clients.getAll); - router.post('/api/v1/clients', token, authorizeAdmin, routes.clients.add); - router.get ('/api/v1/clients/:clientId', token, authorizeAdmin, routes.clients.get); - router.post('/api/v1/clients/:clientId', token, authorizeAdmin, routes.clients.add); - router.del ('/api/v1/clients/:clientId', token, authorizeAdmin, routes.clients.del); - router.get ('/api/v1/clients/:clientId/tokens', token, authorizeAdmin, routes.clients.getTokens); - router.post('/api/v1/clients/:clientId/tokens', token, authorizeAdmin, routes.clients.addToken); - router.del ('/api/v1/clients/:clientId/tokens', token, authorizeAdmin, routes.clients.delTokens); - router.del ('/api/v1/clients/:clientId/tokens/:tokenId', token, authorizeAdmin, routes.clients.delToken); - // appstore and subscription routes router.post('/api/v1/appstore/register_cloudron', token, authorizeAdmin, routes.appstore.registerCloudron); router.get ('/api/v1/appstore/subscription', token, authorizeAdmin, routes.appstore.getSubscription); @@ -231,7 +220,7 @@ function initializeExpressSync() { router.get ('/api/v1/apps/:id/logs', token, authorizeAdmin, routes.apps.getLogs); router.get ('/api/v1/apps/:id/exec', token, authorizeAdmin, routes.apps.exec); // websocket cannot do bearer authentication - router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, ROLE_ADMIN), routes.apps.execWebSocket); + router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, accesscontrol.ROLE_ADMIN), routes.apps.execWebSocket); router.post('/api/v1/apps/:id/clone', token, authorizeAdmin, routes.apps.cloneApp); router.get ('/api/v1/apps/:id/download', token, authorizeAdmin, routes.apps.downloadFile); router.post('/api/v1/apps/:id/upload', token, authorizeAdmin, multipart, routes.apps.uploadFile); diff --git a/src/test/accesscontrol-test.js b/src/test/accesscontrol-test.js index 7d117031c..6276ae553 100644 --- a/src/test/accesscontrol-test.js +++ b/src/test/accesscontrol-test.js @@ -10,71 +10,11 @@ var accesscontrol = require('../accesscontrol.js'), expect = require('expect.js'); describe('access control', function () { - describe('canonicalScopeString', function () { - it('only * scope', function () { - expect(accesscontrol.canonicalScopeString('*')).to.be(accesscontrol.VALID_SCOPES.join(',')); - }); - - it('identity for non-*', function () { - expect(accesscontrol.canonicalScopeString('foo,bar')).to.be('bar,foo'); // becomes sorted - }); + describe('verifyToken', function () { + // FIXME }); - describe('intersectScopes', function () { // args: allowed, wanted - it('both are same', function () { - expect(accesscontrol.intersectScopes([ 'apps', 'clients' ], [ 'apps', 'clients' ])).to.eql([ 'apps', 'clients' ]); - }); - - it('some are different', function () { - expect(accesscontrol.intersectScopes([ 'apps' ], [ 'apps', 'clients' ])).to.eql(['apps']); - expect(accesscontrol.intersectScopes([ 'clients', 'domains', 'mail' ], [ 'mail' ])).to.eql(['mail']); - }); - - it('everything is different', function () { - expect(accesscontrol.intersectScopes(['cloudron', 'domains' ], ['apps','clients'])).to.eql([]); - }); - - it('subscopes', function () { - expect(accesscontrol.intersectScopes(['apps:read' ], ['apps'])).to.eql(['apps:read']); - expect(accesscontrol.intersectScopes(['apps:read','domains','profile'], ['apps','domains:manage','profile'])).to.eql(['apps:read','domains:manage','profile']); - expect(accesscontrol.intersectScopes(['apps:read','domains','profile'], ['apps','apps:read'])).to.eql(['apps:read']); - }); - }); - - describe('validateScopeString', function () { - it('allows valid scopes', function () { - expect(accesscontrol.validateScopeString('apps')).to.be(null); - expect(accesscontrol.validateScopeString('apps,mail')).to.be(null); - expect(accesscontrol.validateScopeString('apps:read,mail')).to.be(null); - expect(accesscontrol.validateScopeString('apps,mail:write')).to.be(null); - }); - - it('disallows invalid scopes', function () { - expect(accesscontrol.validateScopeString('apps, mail')).to.be.an(Error); - expect(accesscontrol.validateScopeString('random')).to.be.an(Error); - expect(accesscontrol.validateScopeString('')).to.be.an(Error); - }); - }); - - describe('hasScopes', function () { - it('succeeds if it contains the scope', function () { - expect(accesscontrol.hasScopes([ 'apps' ], [ 'apps' ])).to.be(null); - expect(accesscontrol.hasScopes([ 'apps', 'mail' ], [ 'mail' ])).to.be(null); - expect(accesscontrol.hasScopes([ 'clients', '*', 'apps', 'mail' ], [ 'mail' ])).to.be(null); - - // subscope - expect(accesscontrol.hasScopes([ 'apps' ], [ 'apps:read' ])).to.be(null); - expect(accesscontrol.hasScopes([ 'apps:read' ], [ 'apps:read' ])).to.be(null); - expect(accesscontrol.hasScopes([ 'apps' , 'mail' ], [ 'apps:*' ])).to.be(null); - expect(accesscontrol.hasScopes([ '*' ], [ 'apps:read' ])).to.be(null); - }); - - it('fails if it does not contain the scope', function () { - expect(accesscontrol.hasScopes([ 'apps' ], [ 'mail' ])).to.be.an(Error); - expect(accesscontrol.hasScopes([ 'apps', 'mail' ], [ 'clients' ])).to.be.an(Error); - - // subscope - expect(accesscontrol.hasScopes([ 'apps:write' ], [ 'apps:read' ])).to.be.an(Error); - }); + describe('hasRole', function () { + // FIXME }); }); diff --git a/src/test/database-test.js b/src/test/database-test.js index 002d19952..f87805e3b 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -10,7 +10,6 @@ var appdb = require('../appdb.js'), async = require('async'), backupdb = require('../backupdb.js'), BoxError = require('../boxerror.js'), - clientdb = require('../clientdb.js'), database = require('../database'), domaindb = require('../domaindb'), eventlogdb = require('../eventlogdb.js'), @@ -723,7 +722,7 @@ describe('database', function () { identifier: '0', clientId: 'clientid-0', expires: Date.now() + 60 * 60000, - scope: 'clients' + scope: '' }; var TOKEN_1 = { id: 'tid-1', @@ -732,7 +731,7 @@ describe('database', function () { identifier: '1', clientId: 'clientid-1', expires: Number.MAX_SAFE_INTEGER, - scope: 'settings' + scope: '' }; var TOKEN_2 = { id: 'tid-2', @@ -741,7 +740,7 @@ describe('database', function () { identifier: '2', clientId: 'clientid-2', expires: Date.now(), - scope: 'apps' + scope: '' }; it('add succeeds', function (done) { @@ -1318,115 +1317,6 @@ describe('database', function () { }); }); - describe('client', function () { - var CLIENT_0 = { - id: 'cid-0', - appId: 'someappid_0', - type: 'typeisastring', - clientSecret: 'secret-0', - redirectURI: 'http://foo.bar', - scope: '*' - - }; - var CLIENT_1 = { - id: 'cid-1', - appId: 'someappid_1', - type: 'typeisastring', - clientSecret: 'secret-', - redirectURI: 'http://foo.bar', - scope: '*' - }; - - it('add succeeds', function (done) { - clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) { - expect(error).to.be(null); - - clientdb.add(CLIENT_1.id, CLIENT_1.appId, CLIENT_0.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope, function (error) { - expect(error).to.be(null); - done(); - }); - }); - }); - - it('add same client id fails', function (done) { - clientdb.add(CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope, function (error) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.ALREADY_EXISTS); - done(); - }); - }); - - it('get succeeds', function (done) { - clientdb.get(CLIENT_0.id, function (error, result) { - expect(error).to.be(null); - expect(result).to.eql(CLIENT_0); - done(); - }); - }); - - it('getByAppId succeeds', function (done) { - clientdb.getByAppId(CLIENT_0.appId, function (error, result) { - expect(error).to.be(null); - expect(result).to.eql(CLIENT_0); - done(); - }); - }); - - it('getByAppIdAndType succeeds', function (done) { - clientdb.getByAppIdAndType(CLIENT_0.appId, CLIENT_0.type, function (error, result) { - expect(error).to.be(null); - expect(result).to.eql(CLIENT_0); - done(); - }); - }); - - it('getByAppId fails for unknown client id', function (done) { - clientdb.getByAppId(CLIENT_0.appId + CLIENT_0.appId, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - - it('getAll succeeds', function (done) { - clientdb.getAll(function (error, result) { - expect(error).to.be(null); - expect(result).to.be.an(Array); - expect(result.length).to.equal(5); // three built-in clients - expect(result[3]).to.eql(CLIENT_0); - expect(result[4]).to.eql(CLIENT_1); - done(); - }); - }); - - it('delByAppIdAndType succeeds', function (done) { - clientdb.delByAppIdAndType(CLIENT_1.appId, CLIENT_1.type, function (error) { - expect(error).to.be(null); - - clientdb.getByAppIdAndType(CLIENT_1.appId, CLIENT_1.type, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - }); - - it('delByAppId succeeds', function (done) { - clientdb.delByAppId(CLIENT_0.appId, function (error) { - expect(error).to.be(null); - - clientdb.getByAppId(CLIENT_0.appId, function (error, result) { - expect(error).to.be.a(BoxError); - expect(error.reason).to.equal(BoxError.NOT_FOUND); - expect(result).to.not.be.ok(); - done(); - }); - }); - }); - }); - describe('settings', function () { it('can set value', function (done) { settingsdb.set('somekey', 'somevalue', function (error) { diff --git a/src/tokens.js b/src/tokens.js new file mode 100644 index 000000000..73242d0c7 --- /dev/null +++ b/src/tokens.js @@ -0,0 +1,58 @@ +'use strict'; + +exports = module.exports = { + addTokenByUserId: addTokenByUserId, + + // token client ids. we categorize them so we can have different restrictions based on the client + ID_WEBADMIN: 'cid-webadmin', // dashboard oauth + ID_SDK: 'cid-sdk', // created by user via dashboard + ID_CLI: 'cid-cli' // created via cli tool +}; + +let assert = require('assert'), + BoxError = require('./boxerror.js'), + hat = require('./hat.js'), + uuid = require('uuid'), + tokendb = require('./tokendb.js'); + +function validateTokenName(name) { + assert.strictEqual(typeof name, 'string'); + + if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' }); + + return null; +} + +function addTokenByUserId(clientId, userId, expiresAt, options, callback) { + assert.strictEqual(typeof clientId, 'string'); + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof expiresAt, 'number'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + const name = options.name || ''; + let error = validateTokenName(name); + if (error) return callback(error); + + const token = { + id: 'tid-' + uuid.v4(), + accessToken: hat(8 * 32), + identifier: userId, + clientId: clientId, + expires: expiresAt, + scope: 'unused', + name: name + }; + + tokendb.add(token, function (error) { + if (error) return callback(error); + + callback(null, { + accessToken: token.accessToken, + tokenScopes: 'unused', + identifier: userId, + clientId: clientId, + expires: expiresAt + }); + }); +}