diff --git a/CHANGES b/CHANGES index 2e4661427..6a35ea447 100644 --- a/CHANGES +++ b/CHANGES @@ -2786,3 +2786,5 @@ * IPv6 only server installation * Initial Ubuntu 24.04 (Noble Numbat) support * syslog: handle potential multiline syslog input +* user directory: fixes to mandatory 2fa setting when cloudron connector is used + diff --git a/dashboard/src/js/index.js b/dashboard/src/js/index.js index de48b4def..a76d03937 100644 --- a/dashboard/src/js/index.js +++ b/dashboard/src/js/index.js @@ -677,7 +677,10 @@ app.controller('MainController', ['$scope', '$route', '$timeout', '$location', ' }; function redirectOnMandatory2FA() { - if (Client.getConfig().mandatory2FA && !Client.getUserInfo().twoFactorAuthenticationEnabled) { + if (Client.getConfig().mandatory2FA) { + if (Client.getUserInfo().twoFactorAuthenticationEnabled) return; // user already has 2fa + if (Client.getUserInfo().source && $scope.config.external2FA) return; // 2fa is external + $location.path('/profile').search({ setup2fa: true }); } } diff --git a/dashboard/src/views/user-settings.js b/dashboard/src/views/user-settings.js index 416bdf577..c59cb0ec3 100644 --- a/dashboard/src/views/user-settings.js +++ b/dashboard/src/views/user-settings.js @@ -44,9 +44,6 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$ }, submit: function () { - // prevent the current user from getting locked out - if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) return Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile'); - $scope.profileConfig.error = ''; $scope.profileConfig.busy = true; $scope.profileConfig.success = false; @@ -68,6 +65,12 @@ angular.module('Application').controller('UserSettingsController', ['$scope', '$ $timeout(function () { $scope.profileConfig.busy = false; + + // prevent the current user from getting locked out. if user ignores this, they have to use cloudron-support --admin-login + if ($scope.profileConfig.mandatory2FA && !$scope.userInfo.twoFactorAuthenticationEnabled) { + if ($scope.userInfo.source && $scope.config.external2FA) return; // no need for warning if 2fa is external + Client.notify('', $translate.instant('users.settings.require2FAWarning'), true, 'error', '#/profile'); + } }, 500); }); } diff --git a/src/routes/test/common.js b/src/routes/test/common.js index 8c85f9d7a..5b3c21ab7 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -94,7 +94,7 @@ async function setup() { expect(response.status).to.equal(201); admin.id = response.body.id; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - const token1 = await tokens.add({ identifier: admin.id, clientId: 'test-client-id', expires: Date.now() + (60 * 60 * 1000), name: 'fromtest' }); + const token1 = await tokens.add({ identifier: admin.id, clientId: tokens.ID_WEBADMIN, expires: Date.now() + (60 * 60 * 1000), name: 'fromtest' }); admin.token = token1.accessToken; // create user @@ -104,7 +104,7 @@ async function setup() { expect(response.status).to.equal(201); user.id = response.body.id; // HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...) - const token2 = await tokens.add({ identifier: user.id, clientId: 'test-client-id', expires: Date.now() + (60 * 60 * 1000), name: 'fromtest' }); + const token2 = await tokens.add({ identifier: user.id, clientId: tokens.ID_WEBADMIN, expires: Date.now() + (60 * 60 * 1000), name: 'fromtest' }); user.token = token2.accessToken; await settings._set(settings.APPSTORE_API_TOKEN_KEY, exports.appstoreToken); // appstore token diff --git a/src/routes/test/user-directory-test.js b/src/routes/test/user-directory-test.js index d6a9635fa..5be31d151 100644 --- a/src/routes/test/user-directory-test.js +++ b/src/routes/test/user-directory-test.js @@ -74,7 +74,13 @@ describe('User Directory API', function () { .query({ access_token: owner.token }) .ok(() => true); - expect(response2.statusCode).to.equal(401); // token is gone + expect(response2.statusCode).to.equal(200); // token is not gone, since it is persisted + + const response3 = await superagent.get(`${serverUrl}/api/v1/profile`) + .query({ access_token: user.token }) + .ok(() => true); + + expect(response3.statusCode).to.equal(401); // token is gone }); }); }); diff --git a/src/routes/user-directory.js b/src/routes/user-directory.js index 861d1611d..99bbce41c 100644 --- a/src/routes/user-directory.js +++ b/src/routes/user-directory.js @@ -26,7 +26,7 @@ async function setProfileConfig(req, res, next) { 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(userDirectory.setProfileConfig(req.body, AuditSource.fromRequest(req))); + const [error] = await safe(userDirectory.setProfileConfig(req.body, { persistUserIdSessions: req.user.id }, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); diff --git a/src/test/user-directory-test.js b/src/test/user-directory-test.js index 5f9a83e5d..72b88aa6e 100644 --- a/src/test/user-directory-test.js +++ b/src/test/user-directory-test.js @@ -28,7 +28,7 @@ describe('User Directory', function () { let result = await tokens.listByUserId(admin.id); expect(result.length).to.be(1); // just confirm the token was really added! - await userDirectory.setProfileConfig({ mandatory2FA: true, lockUserProfiles: true }, auditSource); + await userDirectory.setProfileConfig({ mandatory2FA: true, lockUserProfiles: true }, { persistUserIdSessions: 'random' }, auditSource); 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/user-directory.js b/src/user-directory.js index 83e6e2d2e..b05dca068 100644 --- a/src/user-directory.js +++ b/src/user-directory.js @@ -20,8 +20,9 @@ async function getProfileConfig() { return value || { lockUserProfiles: false, mandatory2FA: false }; } -async function setProfileConfig(profileConfig, auditSource) { +async function setProfileConfig(profileConfig, options, auditSource) { assert.strictEqual(typeof profileConfig, 'object'); + assert.strictEqual(typeof options, 'object'); assert(auditSource && typeof auditSource === 'object'); if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode'); @@ -35,8 +36,10 @@ async function setProfileConfig(profileConfig, auditSource) { debug('setProfileConfig: logging out non-2FA users to enforce 2FA'); const allUsers = await users.list(); + for (const user of allUsers) { if (user.twoFactorAuthenticationEnabled) continue; + if (options.persistUserIdSessions === user.id) continue; // do not logout the API caller await tokens.delByUserIdAndType(user.id, tokens.ID_WEBADMIN); await oidc.revokeByUserId(user.id);