Make it possible to enforce mandatory 2FA for groups

Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2018-10-11 12:20:18 +02:00
parent 82a5833217
commit 83e994c11f
No known key found for this signature in database
GPG Key ID: CC42AC2A7F0E56D8
29 changed files with 729 additions and 159 deletions

View File

@ -26,6 +26,8 @@ declare(strict_types=1);
namespace OC\Core\Command\TwoFactorAuth; namespace OC\Core\Command\TwoFactorAuth;
use function implode;
use OC\Authentication\TwoFactorAuth\EnforcementState;
use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -58,17 +60,32 @@ class Enforce extends Command {
InputOption::VALUE_NONE, InputOption::VALUE_NONE,
'don\'t enforce two-factor authenticaton' 'don\'t enforce two-factor authenticaton'
); );
$this->addOption(
'group',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'enforce only for the given group(s)'
);
$this->addOption(
'exclude',
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'exclude mandatory two-factor auth for the given group(s)'
);
} }
protected function execute(InputInterface $input, OutputInterface $output) { protected function execute(InputInterface $input, OutputInterface $output) {
if ($input->getOption('on')) { if ($input->getOption('on')) {
$this->mandatoryTwoFactor->setEnforced(true); $enforcedGroups = $input->getOption('group');
$excludedGroups = $input->getOption('exclude');
$this->mandatoryTwoFactor->setState(new EnforcementState(true, $enforcedGroups, $excludedGroups));
} elseif ($input->getOption('off')) { } elseif ($input->getOption('off')) {
$this->mandatoryTwoFactor->setEnforced(false); $this->mandatoryTwoFactor->setState(new EnforcementState(false));
} }
if ($this->mandatoryTwoFactor->isEnforced()) { $state = $this->mandatoryTwoFactor->getState();
$this->writeEnforced($output); if ($state->isEnforced()) {
$this->writeEnforced($output, $state);
} else { } else {
$this->writeNotEnforced($output); $this->writeNotEnforced($output);
} }
@ -77,8 +94,16 @@ class Enforce extends Command {
/** /**
* @param OutputInterface $output * @param OutputInterface $output
*/ */
protected function writeEnforced(OutputInterface $output) { protected function writeEnforced(OutputInterface $output, EnforcementState $state) {
$output->writeln('Two-factor authentication is enforced for all users'); if (empty($state->getEnforcedGroups())) {
$message = 'Two-factor authentication is enforced for all users';
} else {
$message = 'Two-factor authentication is enforced for members of the group(s) ' . implode(', ', $state->getEnforcedGroups());
}
if (!empty($state->getExcludedGroups())) {
$message .= ', except members of ' . implode(', ', $state->getExcludedGroups());
}
$output->writeln($message);
} }
/** /**

View File

@ -460,6 +460,7 @@ return array(
'OC\\Authentication\\Token\\PublicKeyTokenMapper' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php',
'OC\\Authentication\\Token\\PublicKeyTokenProvider' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php',
'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php', 'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php',
'OC\\Authentication\\TwoFactorAuth\\EnforcementState' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/EnforcementState.php',
'OC\\Authentication\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php',
'OC\\Authentication\\TwoFactorAuth\\MandatoryTwoFactor' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/MandatoryTwoFactor.php', 'OC\\Authentication\\TwoFactorAuth\\MandatoryTwoFactor' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/MandatoryTwoFactor.php',
'OC\\Authentication\\TwoFactorAuth\\ProviderLoader' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php', 'OC\\Authentication\\TwoFactorAuth\\ProviderLoader' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php',

View File

@ -490,6 +490,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Authentication\\Token\\PublicKeyTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php',
'OC\\Authentication\\Token\\PublicKeyTokenProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php',
'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php', 'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php',
'OC\\Authentication\\TwoFactorAuth\\EnforcementState' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/EnforcementState.php',
'OC\\Authentication\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php',
'OC\\Authentication\\TwoFactorAuth\\MandatoryTwoFactor' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/MandatoryTwoFactor.php', 'OC\\Authentication\\TwoFactorAuth\\MandatoryTwoFactor' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/MandatoryTwoFactor.php',
'OC\\Authentication\\TwoFactorAuth\\ProviderLoader' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php', 'OC\\Authentication\\TwoFactorAuth\\ProviderLoader' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php',

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OC\Authentication\TwoFactorAuth;
use JsonSerializable;
class EnforcementState implements JsonSerializable {
/** @var bool */
private $enforced;
/** @var array */
private $enforcedGroups;
/** @var array */
private $excludedGroups;
/**
* EnforcementState constructor.
*
* @param bool $enforced
* @param string[] $enforcedGroups
* @param string[] $excludedGroups
*/
public function __construct(bool $enforced,
array $enforcedGroups = [],
array $excludedGroups = []) {
$this->enforced = $enforced;
$this->enforcedGroups = $enforcedGroups;
$this->excludedGroups = $excludedGroups;
}
/**
* @return string[]
*/
public function isEnforced(): bool {
return $this->enforced;
}
/**
* @return string[]
*/
public function getEnforcedGroups(): array {
return $this->enforcedGroups;
}
/**
* @return string[]
*/
public function getExcludedGroups(): array {
return $this->excludedGroups;
}
public function jsonSerialize(): array {
return [
'enforced' => $this->enforced,
'enforcedGroups' => $this->enforcedGroups,
'excludedGroups' => $this->excludedGroups,
];
}
}

View File

@ -106,7 +106,7 @@ class Manager {
* @return boolean * @return boolean
*/ */
public function isTwoFactorAuthenticated(IUser $user): bool { public function isTwoFactorAuthenticated(IUser $user): bool {
if ($this->mandatoryTwoFactor->isEnforced()) { if ($this->mandatoryTwoFactor->isEnforcedFor($user)) {
return true; return true;
} }

View File

@ -27,22 +27,89 @@ declare(strict_types=1);
namespace OC\Authentication\TwoFactorAuth; namespace OC\Authentication\TwoFactorAuth;
use OCP\IConfig; use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUser;
class MandatoryTwoFactor { class MandatoryTwoFactor {
/** @var IConfig */ /** @var IConfig */
private $config; private $config;
public function __construct(IConfig $config) { /** @var IGroupManager */
private $groupManager;
public function __construct(IConfig $config, IGroupManager $groupManager) {
$this->config = $config; $this->config = $config;
$this->groupManager = $groupManager;
} }
public function isEnforced(): bool { /**
return $this->config->getSystemValue('twofactor_enforced', 'false') === 'true'; * Get the state of enforced two-factor auth
*/
public function getState(): EnforcementState {
return new EnforcementState(
$this->config->getSystemValue('twofactor_enforced', 'false') === 'true',
$this->config->getSystemValue('twofactor_enforced_groups', []),
$this->config->getSystemValue('twofactor_enforced_excluded_groups', [])
);
} }
public function setEnforced(bool $enforced) { /**
$this->config->setSystemValue('twofactor_enforced', $enforced ? 'true' : 'false'); * Set the state of enforced two-factor auth
*/
public function setState(EnforcementState $state) {
$this->config->setSystemValue('twofactor_enforced', $state->isEnforced() ? 'true' : 'false');
$this->config->setSystemValue('twofactor_enforced_groups', $state->getEnforcedGroups());
$this->config->setSystemValue('twofactor_enforced_excluded_groups', $state->getExcludedGroups());
} }
/**
* Check if two-factor auth is enforced for a specific user
*
* The admin(s) can enforce two-factor auth system-wide, for certain groups only
* and also have the option to exclude users of certain groups. This method will
* check their membership of those groups.
*
* @param IUser $user
*
* @return bool
*/
public function isEnforcedFor(IUser $user): bool {
$state = $this->getState();
if (!$state->isEnforced()) {
return false;
}
$uid = $user->getUID();
/*
* If there is a list of enforced groups, we only enforce 2FA for members of those groups.
* For all the other users it is not enforced (overruling the excluded groups list).
*/
if (!empty($state->getEnforcedGroups())) {
foreach ($state->getEnforcedGroups() as $group) {
if ($this->groupManager->isInGroup($uid, $group)) {
return true;
}
}
// Not a member of any of these groups -> no 2FA enforced
return false;
}
/**
* If the user is member of an excluded group, 2FA won't be enforced.
*/
foreach ($state->getExcludedGroups() as $group) {
if ($this->groupManager->isInGroup($uid, $group)) {
return false;
}
}
/**
* No enforced groups configured and user not member of an excluded groups,
* so 2FA is enforced.
*/
return true;
}
} }

View File

@ -26,12 +26,11 @@ declare(strict_types=1);
namespace OC\Settings\Controller; namespace OC\Settings\Controller;
use OC\Authentication\TwoFactorAuth\EnforcementState;
use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\IRequest; use OCP\IRequest;
use OCP\JSON;
class TwoFactorSettingsController extends Controller { class TwoFactorSettingsController extends Controller {
@ -46,18 +45,16 @@ class TwoFactorSettingsController extends Controller {
$this->mandatoryTwoFactor = $mandatoryTwoFactor; $this->mandatoryTwoFactor = $mandatoryTwoFactor;
} }
public function index(): Response { public function index(): JSONResponse {
return new JSONResponse([ return new JSONResponse($this->mandatoryTwoFactor->getState());
'enabled' => $this->mandatoryTwoFactor->isEnforced(),
]);
} }
public function update(bool $enabled): Response { public function update(bool $enforced, array $enforcedGroups = [], array $excludedGroups = []): JSONResponse {
$this->mandatoryTwoFactor->setEnforced($enabled); $this->mandatoryTwoFactor->setState(
new EnforcementState($enforced, $enforcedGroups, $excludedGroups)
);
return new JSONResponse([ return new JSONResponse($this->mandatoryTwoFactor->getState());
'enabled' => $enabled
]);
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
settings/js/1.js.map Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

33
settings/js/6.js Normal file

File diff suppressed because one or more lines are too long

1
settings/js/6.js.map Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3292,8 +3292,7 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -3314,14 +3313,12 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -3336,20 +3333,17 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -3466,8 +3460,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -3479,7 +3472,6 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -3494,7 +3486,6 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -3502,14 +3493,12 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.2.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.1",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -3528,7 +3517,6 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -3609,8 +3597,7 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -3622,7 +3609,6 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -3708,8 +3694,7 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -3745,7 +3730,6 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -3765,7 +3749,6 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -3809,14 +3792,12 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true
"optional": true
} }
} }
}, },
@ -4689,10 +4670,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.5", "version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"dev": true
}, },
"lodash.assign": { "lodash.assign": {
"version": "4.2.0", "version": "4.2.0",

View File

@ -12,6 +12,7 @@
}, },
"dependencies": { "dependencies": {
"@babel/polyfill": "^7.0.0", "@babel/polyfill": "^7.0.0",
"lodash": "^4.17.11",
"nextcloud-axios": "^0.1.2", "nextcloud-axios": "^0.1.2",
"nextcloud-vue": "^0.2.0", "nextcloud-vue": "^0.2.0",
"v-tooltip": "^2.0.0-rc.33", "v-tooltip": "^2.0.0-rc.33",

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<p> <p>
{{ t('settings', 'Two-factor authentication can be enforced for all users. If they do not have a two-factor provider configured, they will be unable to log into the system.') }} {{ t('settings', 'Two-factor authentication can be enforced for all users and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system.') }}
</p> </p>
<p v-if="loading"> <p v-if="loading">
<span class="icon-loading-small two-factor-loading"></span> <span class="icon-loading-small two-factor-loading"></span>
@ -11,22 +11,74 @@
<input type="checkbox" <input type="checkbox"
id="two-factor-enforced" id="two-factor-enforced"
class="checkbox" class="checkbox"
v-model="enabled" v-model="state.enforced"
v-on:change="onEnforcedChanged"> v-on:change="saveChanges">
<label for="two-factor-enforced">{{ t('settings', 'Enforce two-factor authentication') }}</label> <label for="two-factor-enforced">{{ t('settings', 'Enforce two-factor authentication') }}</label>
</p> </p>
<h3>{{ t('settings', 'Limit to groups') }}</h3>
{{ t('settings', 'Enforcement of two-factor authentication can be set for certain groups only.') }}
<p>
{{ t('settings', 'Two-factor authentication is enforced for all members of the following groups.') }}
</p>
<p>
<Multiselect v-model="state.enforcedGroups"
:options="groups"
:placeholder="t('settings', 'Enforced groups')"
:disabled="loading"
:multiple="true"
:searchable="true"
@search-change="searchGroup"
:loading="loadingGroups"
:show-no-options="false"
:close-on-select="false">
</Multiselect>
</p>
<p>
{{ t('settings', 'Two-factor authentication is not enforced for members of the following groups.') }}
</p>
<p>
<Multiselect v-model="state.excludedGroups"
:options="groups"
:placeholder="t('settings', 'Excluded groups')"
:disabled="loading"
:multiple="true"
:searchable="true"
@search-change="searchGroup"
:loading="loadingGroups"
:show-no-options="false"
:close-on-select="false">
</Multiselect>
</p>
<p>
<button class="button primary"
v-on:click="saveChanges"
:disabled="loading">
{{ t('settings', 'Save changes') }}
</button>
</p>
</div> </div>
</template> </template>
<script> <script>
import Axios from 'nextcloud-axios' import Axios from 'nextcloud-axios'
import {Multiselect} from 'nextcloud-vue'
import _ from 'lodash'
export default { export default {
name: "AdminTwoFactor", name: "AdminTwoFactor",
components: {
Multiselect
},
data () { data () {
return { return {
enabled: false, state: {
loading: false enforced: false,
enforcedGroups: [],
excludedGroups: [],
},
loading: false,
groups: [],
loadingGroups: false,
} }
}, },
mounted () { mounted () {
@ -34,33 +86,45 @@
Axios.get(OC.generateUrl('/settings/api/admin/twofactorauth')) Axios.get(OC.generateUrl('/settings/api/admin/twofactorauth'))
.then(resp => resp.data) .then(resp => resp.data)
.then(state => { .then(state => {
this.enabled = state.enabled this.state = state
// Groups are loaded dynamically, but the assigned ones *should*
// be valid groups, so let's add them as initial state
this.groups = _.sortedUniq(this.state.enforcedGroups.concat(this.state.excludedGroups))
this.loading = false this.loading = false
console.info('loaded')
}) })
.catch(err => { .catch(err => {
console.error(error) console.error('Could not load two-factor state', err)
this.loading = false
throw err throw err
}) })
}, },
methods: { methods: {
onEnforcedChanged () { searchGroup: _.debounce(function (query) {
this.loadingGroups = true
Axios.get(OC.linkToOCS(`cloud/groups?offset=0&search=${encodeURIComponent(query)}&limit=20`, 2))
.then(res => res.data.ocs)
.then(ocs => ocs.data.groups)
.then(groups => this.groups = _.sortedUniq(this.groups.concat(groups)))
.catch(err => console.error('could not search groups', err))
.then(() => this.loadingGroups = false)
}, 500),
saveChanges () {
this.loading = true this.loading = true
const data = {
enabled: this.enabled const oldState = this.state
}
Axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), data) Axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), this.state)
.then(resp => resp.data) .then(resp => resp.data)
.then(state => { .then(state => this.state = state)
this.enabled = state.enabled
this.loading = false
})
.catch(err => { .catch(err => {
console.error(error) console.error('could not save changes', err)
this.loading = false
throw err // Restore
this.state = oldState
}) })
.then(() => this.loading = false)
} }
} }
} }

View File

@ -26,6 +26,7 @@ declare(strict_types=1);
namespace Tests\Core\Command\TwoFactorAuth; namespace Tests\Core\Command\TwoFactorAuth;
use OC\Authentication\TwoFactorAuth\EnforcementState;
use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
use OC\Core\Command\TwoFactorAuth\Enforce; use OC\Core\Command\TwoFactorAuth\Enforce;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
@ -51,11 +52,11 @@ class EnforceTest extends TestCase {
public function testEnforce() { public function testEnforce() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('setEnforced') ->method('setState')
->with(true); ->with($this->equalTo(new EnforcementState(true)));
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('getState')
->willReturn(true); ->willReturn(new EnforcementState(true));
$rc = $this->command->execute([ $rc = $this->command->execute([
'--on' => true, '--on' => true,
@ -66,13 +67,49 @@ class EnforceTest extends TestCase {
$this->assertContains("Two-factor authentication is enforced for all users", $display); $this->assertContains("Two-factor authentication is enforced for all users", $display);
} }
public function testEnforceForOneGroup() {
$this->mandatoryTwoFactor->expects($this->once())
->method('setState')
->with($this->equalTo(new EnforcementState(true, ['twofactorers'])));
$this->mandatoryTwoFactor->expects($this->once())
->method('getState')
->willReturn(new EnforcementState(true, ['twofactorers']));
$rc = $this->command->execute([
'--on' => true,
'--group' => ['twofactorers'],
]);
$this->assertEquals(0, $rc);
$display = $this->command->getDisplay();
$this->assertContains("Two-factor authentication is enforced for members of the group(s) twofactorers", $display);
}
public function testEnforceForAllExceptOneGroup() {
$this->mandatoryTwoFactor->expects($this->once())
->method('setState')
->with($this->equalTo(new EnforcementState(true, [], ['yoloers'])));
$this->mandatoryTwoFactor->expects($this->once())
->method('getState')
->willReturn(new EnforcementState(true, [], ['yoloers']));
$rc = $this->command->execute([
'--on' => true,
'--exclude' => ['yoloers'],
]);
$this->assertEquals(0, $rc);
$display = $this->command->getDisplay();
$this->assertContains("Two-factor authentication is enforced for all users, except members of yoloers", $display);
}
public function testDisableEnforced() { public function testDisableEnforced() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('setEnforced') ->method('setState')
->with(false); ->with(new EnforcementState(false));
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('getState')
->willReturn(false); ->willReturn(new EnforcementState(false));
$rc = $this->command->execute([ $rc = $this->command->execute([
'--off' => true, '--off' => true,
@ -85,8 +122,8 @@ class EnforceTest extends TestCase {
public function testCurrentStateEnabled() { public function testCurrentStateEnabled() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('getState')
->willReturn(true); ->willReturn(new EnforcementState(true));
$rc = $this->command->execute([]); $rc = $this->command->execute([]);
@ -97,8 +134,8 @@ class EnforceTest extends TestCase {
public function testCurrentStateDisabled() { public function testCurrentStateDisabled() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('getState')
->willReturn(false); ->willReturn(new EnforcementState(false));
$rc = $this->command->execute([]); $rc = $this->command->execute([]);

View File

@ -22,6 +22,7 @@
namespace Tests\Settings\Controller; namespace Tests\Settings\Controller;
use OC\Authentication\TwoFactorAuth\EnforcementState;
use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
use OC\Settings\Controller\TwoFactorSettingsController; use OC\Settings\Controller\TwoFactorSettingsController;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -54,12 +55,11 @@ class TwoFactorSettingsControllerTest extends TestCase {
} }
public function testIndex() { public function testIndex() {
$state = new EnforcementState(true);
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('getState')
->willReturn(true); ->willReturn($state);
$expected = new JSONResponse([ $expected = new JSONResponse($state);
'enabled' => true,
]);
$resp = $this->controller->index(); $resp = $this->controller->index();
@ -67,12 +67,14 @@ class TwoFactorSettingsControllerTest extends TestCase {
} }
public function testUpdate() { public function testUpdate() {
$state = new EnforcementState(true);
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('setEnforced') ->method('setState')
->with(true); ->with($this->equalTo(new EnforcementState(true)));
$expected = new JSONResponse([ $this->mandatoryTwoFactor->expects($this->once())
'enabled' => true, ->method('getState')
]); ->willReturn($state);
$expected = new JSONResponse($state);
$resp = $this->controller->update(true); $resp = $this->controller->update(true);

View File

@ -0,0 +1,67 @@
<?php
/**
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Created by PhpStorm.
* User: christoph
* Date: 11.10.18
* Time: 13:01
*/
namespace Tests\Authentication\TwoFactorAuth;
use OC\Authentication\TwoFactorAuth\EnforcementState;
use Test\TestCase;
class EnforcementStateTest extends TestCase {
public function testIsEnforced() {
$state = new EnforcementState(true);
$this->assertTrue($state->isEnforced());
}
public function testGetEnforcedGroups() {
$state = new EnforcementState(true, ['twofactorers']);
$this->assertEquals(['twofactorers'], $state->getEnforcedGroups());
}
public function testGetExcludedGroups() {
$state = new EnforcementState(true, [], ['yoloers']);
$this->assertEquals(['yoloers'], $state->getExcludedGroups());
}
public function testJsonSerialize() {
$state = new EnforcementState(true, ['twofactorers'], ['yoloers']);
$expected = [
'enforced' => true,
'enforcedGroups' => ['twofactorers'],
'excludedGroups' => ['yoloers'],
];
$json = $state->jsonSerialize();
$this->assertEquals($expected, $json);
}
}

View File

@ -37,58 +37,59 @@ use OCP\IConfig;
use OCP\ILogger; use OCP\ILogger;
use OCP\ISession; use OCP\ISession;
use OCP\IUser; use OCP\IUser;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Test\TestCase; use Test\TestCase;
class ManagerTest extends TestCase { class ManagerTest extends TestCase {
/** @var IUser|\PHPUnit_Framework_MockObject_MockObject */ /** @var IUser|MockObject */
private $user; private $user;
/** @var ProviderLoader|\PHPUnit_Framework_MockObject_MockObject */ /** @var ProviderLoader|MockObject */
private $providerLoader; private $providerLoader;
/** @var IRegistry|\PHPUnit_Framework_MockObject_MockObject */ /** @var IRegistry|MockObject */
private $providerRegistry; private $providerRegistry;
/** @var MandatoryTwoFactor|\PHPUnit_Framework_MockObject_MockObject */ /** @var MandatoryTwoFactor|MockObject */
private $mandatoryTwoFactor; private $mandatoryTwoFactor;
/** @var ISession|\PHPUnit_Framework_MockObject_MockObject */ /** @var ISession|MockObject */
private $session; private $session;
/** @var Manager */ /** @var Manager */
private $manager; private $manager;
/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ /** @var IConfig|MockObject */
private $config; private $config;
/** @var IManager|\PHPUnit_Framework_MockObject_MockObject */ /** @var IManager|MockObject */
private $activityManager; private $activityManager;
/** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */ /** @var ILogger|MockObject */
private $logger; private $logger;
/** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */ /** @var IProvider|MockObject */
private $fakeProvider; private $fakeProvider;
/** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */ /** @var IProvider|MockObject */
private $backupProvider; private $backupProvider;
/** @var TokenProvider|\PHPUnit_Framework_MockObject_MockObject */ /** @var TokenProvider|MockObject */
private $tokenProvider; private $tokenProvider;
/** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ /** @var ITimeFactory|MockObject */
private $timeFactory; private $timeFactory;
/** @var EventDispatcherInterface|\PHPUnit_Framework_MockObject_MockObject */ /** @var EventDispatcherInterface|MockObject */
private $eventDispatcher; private $eventDispatcher;
protected function setUp() { protected function setUp() {
parent::setUp(); parent::setUp();
$this->user = $this->createMock(IUser::class); $this->user = $this->createMock(IUser::class);
$this->providerLoader = $this->createMock(\OC\Authentication\TwoFactorAuth\ProviderLoader::class); $this->providerLoader = $this->createMock(ProviderLoader::class);
$this->providerRegistry = $this->createMock(IRegistry::class); $this->providerRegistry = $this->createMock(IRegistry::class);
$this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class); $this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class);
$this->session = $this->createMock(ISession::class); $this->session = $this->createMock(ISession::class);
@ -150,7 +151,8 @@ class ManagerTest extends TestCase {
public function testIsTwoFactorAuthenticatedEnforced() { public function testIsTwoFactorAuthenticatedEnforced() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('isEnforcedFor')
->with($this->user)
->willReturn(true); ->willReturn(true);
$enabled = $this->manager->isTwoFactorAuthenticated($this->user); $enabled = $this->manager->isTwoFactorAuthenticated($this->user);
@ -160,7 +162,8 @@ class ManagerTest extends TestCase {
public function testIsTwoFactorAuthenticatedNoProviders() { public function testIsTwoFactorAuthenticatedNoProviders() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('isEnforcedFor')
->with($this->user)
->willReturn(false); ->willReturn(false);
$this->providerRegistry->expects($this->once()) $this->providerRegistry->expects($this->once())
->method('getProviderStates') ->method('getProviderStates')
@ -174,7 +177,8 @@ class ManagerTest extends TestCase {
public function testIsTwoFactorAuthenticatedOnlyBackupCodes() { public function testIsTwoFactorAuthenticatedOnlyBackupCodes() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('isEnforcedFor')
->with($this->user)
->willReturn(false); ->willReturn(false);
$this->providerRegistry->expects($this->once()) $this->providerRegistry->expects($this->once())
->method('getProviderStates') ->method('getProviderStates')
@ -196,7 +200,8 @@ class ManagerTest extends TestCase {
public function testIsTwoFactorAuthenticatedFailingProviders() { public function testIsTwoFactorAuthenticatedFailingProviders() {
$this->mandatoryTwoFactor->expects($this->once()) $this->mandatoryTwoFactor->expects($this->once())
->method('isEnforced') ->method('isEnforcedFor')
->with($this->user)
->willReturn(false); ->willReturn(false);
$this->providerRegistry->expects($this->once()) $this->providerRegistry->expects($this->once())
->method('getProviderStates') ->method('getProviderStates')

View File

@ -26,8 +26,11 @@ declare(strict_types=1);
namespace Tests\Authentication\TwoFactorAuth; namespace Tests\Authentication\TwoFactorAuth;
use OC\Authentication\TwoFactorAuth\EnforcementState;
use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
use OCP\IConfig; use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IUser;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase; use Test\TestCase;
@ -36,6 +39,9 @@ class MandatoryTwoFactorTest extends TestCase {
/** @var IConfig|MockObject */ /** @var IConfig|MockObject */
private $config; private $config;
/** @var IGroupManager|MockObject */
private $groupManager;
/** @var MandatoryTwoFactor */ /** @var MandatoryTwoFactor */
private $mandatoryTwoFactor; private $mandatoryTwoFactor;
@ -43,46 +49,150 @@ class MandatoryTwoFactorTest extends TestCase {
parent::setUp(); parent::setUp();
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->mandatoryTwoFactor = new MandatoryTwoFactor($this->config); $this->mandatoryTwoFactor = new MandatoryTwoFactor($this->config, $this->groupManager);
} }
public function testIsNotEnforced() { public function testIsNotEnforced() {
$this->config->expects($this->once()) $this->config
->method('getSystemValue') ->method('getSystemValue')
->with('twofactor_enforced', 'false') ->willReturnMap([
->willReturn('false'); ['twofactor_enforced', 'false', 'false'],
['twofactor_enforced_groups', [], []],
['twofactor_enforced_excluded_groups', [], []],
]);
$isEnforced = $this->mandatoryTwoFactor->isEnforced(); $state = $this->mandatoryTwoFactor->getState();
$this->assertFalse($state->isEnforced());
}
public function testIsEnforced() {
$this->config
->method('getSystemValue')
->willReturnMap([
['twofactor_enforced', 'false', 'true'],
['twofactor_enforced_groups', [], []],
['twofactor_enforced_excluded_groups', [], []],
]);
$state = $this->mandatoryTwoFactor->getState();
$this->assertTrue($state->isEnforced());
}
public function testIsNotEnforcedForAnybody() {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$this->config
->method('getSystemValue')
->willReturnMap([
['twofactor_enforced', 'false', 'false'],
['twofactor_enforced_groups', [], []],
['twofactor_enforced_excluded_groups', [], []],
]);
$isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user);
$this->assertFalse($isEnforced); $this->assertFalse($isEnforced);
} }
public function testIsEnforced() { public function testIsEnforcedForAGroupMember() {
$this->config->expects($this->once()) $user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$this->config
->method('getSystemValue') ->method('getSystemValue')
->with('twofactor_enforced', 'false') ->willReturnMap([
->willReturn('true'); ['twofactor_enforced', 'false', 'true'],
['twofactor_enforced_groups', [], ['twofactorers']],
['twofactor_enforced_excluded_groups', [], []],
]);
$this->groupManager->method('isInGroup')
->willReturnCallback(function($user, $group) {
return $user === 'user123' && $group ==='twofactorers';
});
$isEnforced = $this->mandatoryTwoFactor->isEnforced(); $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user);
$this->assertTrue($isEnforced); $this->assertTrue($isEnforced);
} }
public function testSetEnforced() { public function testIsEnforcedForOtherGroups() {
$this->config->expects($this->once()) $user = $this->createMock(IUser::class);
->method('setSystemValue') $user->method('getUID')->willReturn('user123');
->with('twofactor_enforced', 'true'); $this->config
->method('getSystemValue')
->willReturnMap([
['twofactor_enforced', 'false', 'true'],
['twofactor_enforced_groups', [], ['twofactorers']],
['twofactor_enforced_excluded_groups', [], []],
]);
$this->groupManager->method('isInGroup')
->willReturn(false);
$this->mandatoryTwoFactor->setEnforced(true); $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user);
$this->assertFalse($isEnforced);
}
public function testIsEnforcedButMemberOfExcludedGroup() {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user123');
$this->config
->method('getSystemValue')
->willReturnMap([
['twofactor_enforced', 'false', 'true'],
['twofactor_enforced_groups', [], []],
['twofactor_enforced_excluded_groups', [], ['yoloers']],
]);
$this->groupManager->method('isInGroup')
->willReturnCallback(function($user, $group) {
return $user === 'user123' && $group ==='yoloers';
});
$isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user);
$this->assertFalse($isEnforced);
}
public function testSetEnforced() {
$this->config
->expects($this->exactly(3))
->method('setSystemValue')
->willReturnMap([
['twofactor_enforced', 'true'],
['twofactor_enforced_groups', []],
['twofactor_enforced_excluded_groups', []],
]);
$this->mandatoryTwoFactor->setState(new EnforcementState(true));
}
public function testSetEnforcedForGroups() {
$this->config
->expects($this->exactly(3))
->method('setSystemValue')
->willReturnMap([
['twofactor_enforced', 'true'],
['twofactor_enforced_groups', ['twofactorers']],
['twofactor_enforced_excluded_groups', ['yoloers']],
]);
$this->mandatoryTwoFactor->setState(new EnforcementState(true, ['twofactorers'], ['yoloers']));
} }
public function testSetNotEnforced() { public function testSetNotEnforced() {
$this->config->expects($this->once()) $this->config
->expects($this->exactly(3))
->method('setSystemValue') ->method('setSystemValue')
->with('twofactor_enforced', 'false'); ->willReturnMap([
['twofactor_enforced', 'false'],
['twofactor_enforced_groups', []],
['twofactor_enforced_excluded_groups', []],
]);
$this->mandatoryTwoFactor->setEnforced(false); $this->mandatoryTwoFactor->setState(new EnforcementState(false));
} }
} }