diff --git a/dashboard/src/js/client.js b/dashboard/src/js/client.js index 5d50e92e2..86b7bba15 100644 --- a/dashboard/src/js/client.js +++ b/dashboard/src/js/client.js @@ -1058,7 +1058,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }; Client.prototype.setProfileConfig = function (config, callback) { - post('/api/v1/settings/profile_config', config, null, function (error, data, status) { + post('/api/v1/user_directory/profile_config', config, null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); @@ -1067,7 +1067,7 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout }; Client.prototype.getProfileConfig = function (callback) { - get('/api/v1/settings/profile_config', null, function (error, data, status) { + get('/api/v1/user_directory/profile_config', null, function (error, data, status) { if (error) return callback(error); if (status !== 200) return callback(new ClientError(status, data)); diff --git a/src/routes/profile.js b/src/routes/profile.js index 822de4a7a..5ddfa3f7e 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -21,14 +21,13 @@ const assert = require('assert'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), - settings = require('../settings.js'), users = require('../users.js'), _ = require('underscore'); async function authorize(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - const [error, profileConfig] = await safe(settings.getProfileConfig()); + const [error, profileConfig] = await safe(users.getProfileConfig()); if (error) return next(BoxError.toHttpError(error)); if (profileConfig.lockUserProfiles) return next(new HttpError(403, 'admin has disallowed users from editing profiles')); diff --git a/src/routes/settings.js b/src/routes/settings.js index 81cd0ccc6..d122e9ed8 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -152,25 +152,6 @@ async function setRegistryConfig(req, res, next) { next(new HttpSuccess(200)); } -async function getProfileConfig(req, res, next) { - const [error, directoryConfig] = await safe(settings.getProfileConfig()); - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, directoryConfig)); -} - -async function setProfileConfig(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (typeof req.body.lockUserProfiles !== 'boolean') return next(new HttpError(400, 'lockUserProfiles is required')); - if (typeof req.body.mandatory2FA !== 'boolean') return next(new HttpError(400, 'mandatory2FA is required')); - - const [error] = await safe(settings.setProfileConfig(req.body)); - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, {})); -} - async function getLanguage(req, res, next) { const [error, language] = await safe(settings.getLanguage()); if (error) return next(BoxError.toHttpError(error)); @@ -201,8 +182,6 @@ function get(req, res, next) { case settings.AUTOUPDATE_PATTERN_KEY: return getAutoupdatePattern(req, res, next); case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next); - case settings.PROFILE_CONFIG_KEY: return getProfileConfig(req, res, next); - default: return next(new HttpError(404, 'No such setting')); } } @@ -218,8 +197,6 @@ function set(req, res, next) { case settings.AUTOUPDATE_PATTERN_KEY: return setAutoupdatePattern(req, res, next); case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next); - case settings.PROFILE_CONFIG_KEY: return setProfileConfig(req, res, next); - default: return next(new HttpError(404, 'No such setting')); } } diff --git a/src/routes/test/cloudron-test.js b/src/routes/test/cloudron-test.js index 05f69b14a..63a56a65e 100644 --- a/src/routes/test/cloudron-test.js +++ b/src/routes/test/cloudron-test.js @@ -159,7 +159,7 @@ describe('Cloudron API', function () { displayName: 'setup user3', }; - const response0 = await superagent.post(`${serverUrl}/api/v1/settings/profile_config`) + const response0 = await superagent.post(`${serverUrl}/api/v1/user_directory/profile_config`) .query({ access_token: owner.token }) .send({ lockUserProfiles: true, mandatory2FA: false }); expect(response0.statusCode).to.equal(200); diff --git a/src/routes/users.js b/src/routes/users.js index 099782363..39cf7d624 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -15,6 +15,9 @@ exports = module.exports = { getPasswordResetLink, sendPasswordResetEmail, + setProfileConfig, + getProfileConfig, + getInviteLink, sendInviteEmail, @@ -263,3 +266,22 @@ async function sendInviteEmail(req, res, next) { next(new HttpSuccess(202, {})); } + +async function getProfileConfig(req, res, next) { + const [error, directoryConfig] = await safe(users.getProfileConfig()); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, directoryConfig)); +} + +async function setProfileConfig(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (typeof req.body.lockUserProfiles !== 'boolean') return next(new HttpError(400, 'lockUserProfiles is required')); + if (typeof req.body.mandatory2FA !== 'boolean') return next(new HttpError(400, 'mandatory2FA is required')); + + const [error] = await safe(users.setProfileConfig(req.body)); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, {})); +} diff --git a/src/server.js b/src/server.js index d4e63dc19..d7fd89ae9 100644 --- a/src/server.js +++ b/src/server.js @@ -202,6 +202,10 @@ async function initializeExpressSync() { router.post('/api/v1/groups/:groupId', json, token, authorizeUserManager, routes.groups.update); router.del ('/api/v1/groups/:groupId', token, authorizeUserManager, routes.groups.remove); + // User directory + router.get ('/api/v1/user_directory/profile_config', token, authorizeAdmin, routes.users.getProfileConfig); + router.post('/api/v1/user_directory/profile_config', json, token, authorizeAdmin, routes.users.setProfileConfig); + // External LDAP router.get ('/api/v1/external_ldap/config', token, authorizeAdmin, routes.externalLdap.getConfig); router.post('/api/v1/external_ldap/config', json, token, authorizeAdmin, routes.externalLdap.setConfig); diff --git a/src/settings.js b/src/settings.js index c294d918d..392cfdd30 100644 --- a/src/settings.js +++ b/src/settings.js @@ -22,12 +22,6 @@ exports = module.exports = { getLanguage, setLanguage, - getProfileConfig, - setProfileConfig, - - getGhosts, - setGhosts, - provider, list, initCache, @@ -117,9 +111,7 @@ const assert = require('assert'), mounts = require('./mounts.js'), paths = require('./paths.js'), safe = require('safetydance'), - tokens = require('./tokens.js'), translation = require('./translation.js'), - users = require('./users.js'), _ = require('underscore'); const SETTINGS_FIELDS = [ 'name', 'value' ].join(','); @@ -144,10 +136,6 @@ const gDefaults = (function () { result[exports.REGISTRY_CONFIG_KEY] = { provider: 'noop' }; - result[exports.PROFILE_CONFIG_KEY] = { - lockUserProfiles: false, - mandatory2FA: false - }; result[exports.DASHBOARD_DOMAIN_KEY] = ''; result[exports.DASHBOARD_FQDN_KEY] = ''; @@ -159,8 +147,6 @@ const gDefaults = (function () { result[exports.CONSOLE_SERVER_ORIGIN_KEY] = 'https://console.cloudron.io'; result[exports.DEMO_KEY] = false; - result[exports.GHOSTS_CONFIG_KEY] = {}; - return result; })(); @@ -347,46 +333,6 @@ async function setRegistryConfig(registryConfig) { notifyChange(exports.REGISTRY_CONFIG_KEY, registryConfig); } -async function getProfileConfig() { - const value = await get(exports.PROFILE_CONFIG_KEY); - if (value === null) return gDefaults[exports.PROFILE_CONFIG_KEY]; - return JSON.parse(value); -} - -async function setProfileConfig(directoryConfig) { - assert.strictEqual(typeof directoryConfig, 'object'); - - if (isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); - - const oldConfig = await getProfileConfig(); - await set(exports.PROFILE_CONFIG_KEY, JSON.stringify(directoryConfig)); - - if (directoryConfig.mandatory2FA && !oldConfig.mandatory2FA) { - debug('setProfileConfig: logging out non-2FA users to enforce 2FA'); - - const allUsers = await users.list(); - for (const user of allUsers) { - if (!user.twoFactorAuthenticationEnabled) await tokens.delByUserIdAndType(user.id, tokens.ID_WEBADMIN); - } - } - - notifyChange(exports.PROFILE_CONFIG_KEY, directoryConfig); -} - -async function getGhosts() { - const value = await get(exports.GHOSTS_CONFIG_KEY); - if (value === null) return gDefaults[exports.GHOSTS_CONFIG_KEY]; - - return JSON.parse(value); -} - -async function setGhosts(ghosts) { - assert.strictEqual(typeof ghosts, 'object'); - - await set(exports.GHOSTS_CONFIG_KEY, JSON.stringify(ghosts)); - notifyChange(exports.GHOSTS_CONFIG_KEY, ghosts); -} - async function getLanguage() { const value = await get(exports.LANGUAGE_KEY); if (value === null) return gDefaults[exports.LANGUAGE_KEY]; @@ -414,7 +360,7 @@ async function list() { result[exports.DEMO_KEY] = !!result[exports.DEMO_KEY]; // convert JSON objects - [exports.BACKUP_POLICY_KEY, exports.BACKUP_CONFIG_KEY, exports.PROFILE_CONFIG_KEY, exports.SERVICES_CONFIG_KEY, + [exports.BACKUP_POLICY_KEY, exports.BACKUP_CONFIG_KEY, exports.SERVICES_CONFIG_KEY, exports.REGISTRY_CONFIG_KEY ].forEach(function (key) { result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]); }); diff --git a/src/test/settings-test.js b/src/test/settings-test.js index 3f39679a7..8ade7ee53 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -9,11 +9,10 @@ const common = require('./common.js'), BoxError = require('../boxerror.js'), expect = require('expect.js'), settings = require('../settings.js'), - tokens = require('../tokens.js'), safe = require('safetydance'); describe('Settings', function () { - const { setup, cleanup, admin } = common; + const { setup, cleanup } = common; before(setup); after(cleanup); @@ -62,22 +61,6 @@ describe('Settings', function () { await settings.setBackupPolicy({ schedule: '00 00 2,23 * * 0,1,2', retention: { keepWithinSecs: 1 }}); }); - it('can get default profile config', async function () { - const profileConfig = await settings.getProfileConfig(); - expect(profileConfig.lockUserProfiles).to.be(false); - expect(profileConfig.mandatory2FA).to.be(false); - }); - - it('can set default profile config', async function () { - await tokens.add({ name: 'token1', identifier: admin.id, clientId: tokens.ID_WEBADMIN, expires: Number.MAX_SAFE_INTEGER, lastUsedTime: null }); - let result = await tokens.listByUserId(admin.id); - expect(result.length).to.be(1); // just confirm the token was really added! - - await settings.setProfileConfig({ mandatory2FA: true, lockUserProfiles: true }); - result = await tokens.listByUserId(admin.id); - expect(result.length).to.be(0); // should have been removed by mandatory 2fa setting change - }); - it('can get all values', async function () { const allSettings = await settings.list(); expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string'); diff --git a/src/test/users-test.js b/src/test/users-test.js index 0aeccd956..7d198ff2f 100644 --- a/src/test/users-test.js +++ b/src/test/users-test.js @@ -9,6 +9,7 @@ const BoxError = require('../boxerror.js'), common = require('./common.js'), expect = require('expect.js'), safe = require('safetydance'), + tokens = require('../tokens.js'), users = require('../users.js'), _ = require('underscore'); @@ -542,4 +543,22 @@ describe('User', function () { it('can re-create user after user was removed', createOwner); }); + + describe('profile config', function () { + it('can get default profile config', async function () { + const profileConfig = await users.getProfileConfig(); + expect(profileConfig.lockUserProfiles).to.be(false); + expect(profileConfig.mandatory2FA).to.be(false); + }); + + it('can set default profile config', async function () { + await tokens.add({ name: 'token1', identifier: admin.id, clientId: tokens.ID_WEBADMIN, expires: Number.MAX_SAFE_INTEGER, lastUsedTime: null }); + let result = await tokens.listByUserId(admin.id); + expect(result.length).to.be(1); // just confirm the token was really added! + + await users.setProfileConfig({ mandatory2FA: true, lockUserProfiles: true }); + result = await tokens.listByUserId(admin.id); + expect(result.length).to.be(0); // should have been removed by mandatory 2fa setting change + }); + }); }); diff --git a/src/users.js b/src/users.js index de7c006a1..6b3776be9 100644 --- a/src/users.js +++ b/src/users.js @@ -51,6 +51,9 @@ exports = module.exports = { getBackgroundImage, setBackgroundImage, + getProfileConfig, + setProfileConfig, + AP_MAIL: 'mail', AP_WEBADMIN: 'webadmin', @@ -273,10 +276,10 @@ async function setGhost(user, password, expiresAt) { debug(`setGhost: ${user.username} expiresAt ${expiresAt}`); - const ghostData = await settings.getGhosts(); + const ghostData = safe.JSON.parse(await settings.get(settings.GHOSTS_CONFIG_KEY)) || {}; ghostData[user.username] = { password, expiresAt }; - await settings.setGhosts(ghostData); + await settings.set(settings.GHOSTS_CONFIG_KEY, JSON.stringify(ghostData)); } // returns true if ghost user was matched @@ -284,7 +287,7 @@ async function verifyGhost(username, password) { assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); - const ghostData = await settings.getGhosts(); + const ghostData = safe.JSON.parse(await settings.get(settings.GHOSTS_CONFIG_KEY)) || {}; // either the username is an object with { password, expiresAt } or a string with the password which will expire on first match if (username in ghostData) { @@ -293,7 +296,7 @@ async function verifyGhost(username, password) { debug('verifyGhost: password expired'); delete ghostData[username]; - await settings.setGhosts(ghostData); + await settings.set(settings.GHOSTS_CONFIG_KEY, JSON.stringify(ghostData)); return false; } else if (ghostData[username].password === password) { @@ -306,8 +309,7 @@ async function verifyGhost(username, password) { debug('verifyGhost: matched ghost user'); delete ghostData[username]; - await settings.setGhosts(ghostData); - + await settings.set(settings.GHOSTS_CONFIG_KEY, JSON.stringify(ghostData)); return true; } } @@ -793,7 +795,7 @@ async function getInviteLink(user, auditSource) { if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory'); if (!user.inviteToken) throw new BoxError(BoxError.BAD_STATE, 'User already used invite link'); - const directoryConfig = await settings.getProfileConfig(); + const directoryConfig = await getProfileConfig(); let inviteLink = `${settings.dashboardOrigin()}/setupaccount.html?inviteToken=${user.inviteToken}&email=${encodeURIComponent(user.email)}`; if (user.username) inviteLink += `&username=${encodeURIComponent(user.username)}`; @@ -820,7 +822,7 @@ async function setupAccount(user, data, auditSource) { assert.strictEqual(typeof data, 'object'); assert(auditSource && typeof auditSource === 'object'); - const profileConfig = await settings.getProfileConfig(); + const profileConfig = await getProfileConfig(); const tmp = { inviteToken: '' }; @@ -948,3 +950,27 @@ async function setBackgroundImage(id, backgroundImage) { const result = await database.query('UPDATE users SET backgroundImage=? WHERE id = ?', [ backgroundImage, id ]); if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); } + +async function getProfileConfig() { + const value = await settings.get(settings.PROFILE_CONFIG_KEY); + if (value === null) return { lockUserProfiles: false, mandatory2FA: false }; + return JSON.parse(value); +} + +async function setProfileConfig(profileConfig) { + assert.strictEqual(typeof profileConfig, 'object'); + + if (settings.isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); + + const oldConfig = await getProfileConfig(); + await settings.set(settings.PROFILE_CONFIG_KEY, JSON.stringify(profileConfig)); + + if (profileConfig.mandatory2FA && !oldConfig.mandatory2FA) { + debug('setProfileConfig: logging out non-2FA users to enforce 2FA'); + + const allUsers = await list(); + for (const user of allUsers) { + if (!user.twoFactorAuthenticationEnabled) await tokens.delByUserIdAndType(user.id, tokens.ID_WEBADMIN); + } + } +}