Procházet zdrojové kódy

Merge pull request #11765 from nextcloud/feature/mandatory-2fa-for-groups

Mandatory 2FA for groups
Morris Jobke před 5 roky
rodič
revize
8177fdb0f6

+ 31 - 6
core/Command/TwoFactorAuth/Enforce.php

@@ -26,6 +26,8 @@ declare(strict_types=1);
 
 namespace OC\Core\Command\TwoFactorAuth;
 
+use function implode;
+use OC\Authentication\TwoFactorAuth\EnforcementState;
 use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
@@ -58,17 +60,32 @@ class Enforce extends Command {
 			InputOption::VALUE_NONE,
 			'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) {
 		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')) {
-			$this->mandatoryTwoFactor->setEnforced(false);
+			$this->mandatoryTwoFactor->setState(new EnforcementState(false));
 		}
 
-		if ($this->mandatoryTwoFactor->isEnforced()) {
-			$this->writeEnforced($output);
+		$state = $this->mandatoryTwoFactor->getState();
+		if ($state->isEnforced()) {
+			$this->writeEnforced($output, $state);
 		} else {
 			$this->writeNotEnforced($output);
 		}
@@ -77,8 +94,16 @@ class Enforce extends Command {
 	/**
 	 * @param OutputInterface $output
 	 */
-	protected function writeEnforced(OutputInterface $output) {
-		$output->writeln('Two-factor authentication is enforced for all users');
+	protected function writeEnforced(OutputInterface $output, EnforcementState $state) {
+		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);
 	}
 
 	/**

+ 1 - 0
lib/composer/composer/autoload_classmap.php

@@ -460,6 +460,7 @@ return array(
     'OC\\Authentication\\Token\\PublicKeyTokenMapper' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenMapper.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\\EnforcementState' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/EnforcementState.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\\ProviderLoader' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php',

+ 1 - 0
lib/composer/composer/autoload_static.php

@@ -490,6 +490,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Authentication\\Token\\PublicKeyTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenMapper.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\\EnforcementState' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/EnforcementState.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\\ProviderLoader' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php',

+ 85 - 0
lib/private/Authentication/TwoFactorAuth/EnforcementState.php

@@ -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,
+		];
+	}
+
+}

+ 1 - 1
lib/private/Authentication/TwoFactorAuth/Manager.php

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

+ 72 - 5
lib/private/Authentication/TwoFactorAuth/MandatoryTwoFactor.php

@@ -27,22 +27,89 @@ declare(strict_types=1);
 namespace OC\Authentication\TwoFactorAuth;
 
 use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IUser;
 
 class MandatoryTwoFactor {
 
 	/** @var IConfig */
 	private $config;
 
-	public function __construct(IConfig $config) {
+	/** @var IGroupManager */
+	private $groupManager;
+
+	public function __construct(IConfig $config, IGroupManager $groupManager) {
 		$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;
+	}
+
+
 }

+ 8 - 11
settings/Controller/TwoFactorSettingsController.php

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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/0.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/0.js.map


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/1.js.map


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/3.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/3.js.map


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/4.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/4.js.map


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 1
settings/js/5.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 25 - 0
settings/js/6.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/6.js.map


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/settings-admin-security.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/settings-admin-security.js.map


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/settings-vue.js


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
settings/js/settings-vue.js.map


+ 14 - 34
settings/package-lock.json

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

+ 1 - 0
settings/package.json

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

+ 85 - 21
settings/src/components/AdminTwoFactor.vue

@@ -1,7 +1,7 @@
 <template>
 	<div>
 		<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 v-if="loading">
 			<span class="icon-loading-small two-factor-loading"></span>
@@ -11,22 +11,74 @@
 			<input type="checkbox"
 				   id="two-factor-enforced"
 				   class="checkbox"
-				   v-model="enabled"
-				   v-on:change="onEnforcedChanged">
+				   v-model="state.enforced"
+				   v-on:change="saveChanges">
 			<label for="two-factor-enforced">{{ t('settings', 'Enforce two-factor authentication') }}</label>
 		</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>
 </template>
 
 <script>
 	import Axios from 'nextcloud-axios'
+	import {Multiselect} from 'nextcloud-vue'
+	import _ from 'lodash'
 
 	export default {
 		name: "AdminTwoFactor",
+		components: {
+			Multiselect
+		},
 		data () {
 			return {
-				enabled: false,
-				loading: false
+				state: {
+					enforced: false,
+					enforcedGroups: [],
+					excludedGroups: [],
+				},
+				loading: false,
+				groups: [],
+				loadingGroups: false,
 			}
 		},
 		mounted () {
@@ -34,33 +86,45 @@
 			Axios.get(OC.generateUrl('/settings/api/admin/twofactorauth'))
 				.then(resp => resp.data)
 				.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
-					console.info('loaded')
 				})
 				.catch(err => {
-					console.error(error)
-					this.loading = false
+					console.error('Could not load two-factor state', err)
 					throw err
 				})
 		},
 		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
-				const data = {
-					enabled: this.enabled
-				}
-				Axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), data)
+
+				const oldState = this.state
+
+				Axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), this.state)
 					.then(resp => resp.data)
-					.then(state => {
-						this.enabled = state.enabled
-						this.loading = false
-					})
+					.then(state => this.state = state)
 					.catch(err => {
-						console.error(error)
-						this.loading = false
-						throw err
+						console.error('could not save changes', err)
+
+						// Restore
+						this.state = oldState
 					})
+					.then(() => this.loading = false)
 			}
 		}
 	}

+ 49 - 12
tests/Core/Command/TwoFactorAuth/EnforceTest.php

@@ -26,6 +26,7 @@ declare(strict_types=1);
 
 namespace Tests\Core\Command\TwoFactorAuth;
 
+use OC\Authentication\TwoFactorAuth\EnforcementState;
 use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
 use OC\Core\Command\TwoFactorAuth\Enforce;
 use PHPUnit\Framework\MockObject\MockObject;
@@ -51,11 +52,11 @@ class EnforceTest extends TestCase {
 
 	public function testEnforce() {
 		$this->mandatoryTwoFactor->expects($this->once())
-			->method('setEnforced')
-			->with(true);
+			->method('setState')
+			->with($this->equalTo(new EnforcementState(true)));
 		$this->mandatoryTwoFactor->expects($this->once())
-			->method('isEnforced')
-			->willReturn(true);
+			->method('getState')
+			->willReturn(new EnforcementState(true));
 
 		$rc = $this->command->execute([
 			'--on' => true,
@@ -66,13 +67,49 @@ class EnforceTest extends TestCase {
 		$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() {
 		$this->mandatoryTwoFactor->expects($this->once())
-			->method('setEnforced')
-			->with(false);
+			->method('setState')
+			->with(new EnforcementState(false));
 		$this->mandatoryTwoFactor->expects($this->once())
-			->method('isEnforced')
-			->willReturn(false);
+			->method('getState')
+			->willReturn(new EnforcementState(false));
 
 		$rc = $this->command->execute([
 			'--off' => true,
@@ -85,8 +122,8 @@ class EnforceTest extends TestCase {
 
 	public function testCurrentStateEnabled() {
 		$this->mandatoryTwoFactor->expects($this->once())
-			->method('isEnforced')
-			->willReturn(true);
+			->method('getState')
+			->willReturn(new EnforcementState(true));
 
 		$rc = $this->command->execute([]);
 
@@ -97,8 +134,8 @@ class EnforceTest extends TestCase {
 
 	public function testCurrentStateDisabled() {
 		$this->mandatoryTwoFactor->expects($this->once())
-			->method('isEnforced')
-			->willReturn(false);
+			->method('getState')
+			->willReturn(new EnforcementState(false));
 
 		$rc = $this->command->execute([]);
 

+ 12 - 10
tests/Settings/Controller/TwoFactorSettingsControllerTest.php

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

+ 67 - 0
tests/lib/Authentication/TwoFactorAuth/EnforcementStateTest.php

@@ -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);
+	}
+}

+ 23 - 18
tests/lib/Authentication/TwoFactorAuth/ManagerTest.php

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

+ 126 - 16
tests/lib/Authentication/TwoFactorAuth/MandatoryTwoFactorTest.php

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

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů