Merge pull request #57216 from Roszakos/2fa-stateless-provider-interface

feat (2fa): Add IStatelessProvider interface
This commit is contained in:
Louis 2026-06-11 11:01:23 +02:00 committed by GitHub
commit 996fcfe443
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 66 additions and 2 deletions

View file

@ -630,6 +630,7 @@
- zorn-v <zorn7@yandex.ru>
- zulan <git@zulan.net>
- Łukasz Buśko <busko.lukasz@pm.me>
- Michał Roszak <m.roszakos@gmail.com>
- Nextcloud GmbH
- ownCloud GmbH
- ownCloud, Inc.

View file

@ -187,6 +187,7 @@ return array(
'OCP\\Authentication\\TwoFactorAuth\\IProvidesIcons' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IProvidesIcons.php',
'OCP\\Authentication\\TwoFactorAuth\\IProvidesPersonalSettings' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IProvidesPersonalSettings.php',
'OCP\\Authentication\\TwoFactorAuth\\IRegistry' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IRegistry.php',
'OCP\\Authentication\\TwoFactorAuth\\IStatelessProvider' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IStatelessProvider.php',
'OCP\\Authentication\\TwoFactorAuth\\RegistryEvent' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/RegistryEvent.php',
'OCP\\Authentication\\TwoFactorAuth\\TwoFactorException' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/TwoFactorException.php',
'OCP\\Authentication\\TwoFactorAuth\\TwoFactorProviderChallengeFailed' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailed.php',

View file

@ -228,6 +228,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Authentication\\TwoFactorAuth\\IProvidesIcons' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IProvidesIcons.php',
'OCP\\Authentication\\TwoFactorAuth\\IProvidesPersonalSettings' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IProvidesPersonalSettings.php',
'OCP\\Authentication\\TwoFactorAuth\\IRegistry' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IRegistry.php',
'OCP\\Authentication\\TwoFactorAuth\\IStatelessProvider' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IStatelessProvider.php',
'OCP\\Authentication\\TwoFactorAuth\\RegistryEvent' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/RegistryEvent.php',
'OCP\\Authentication\\TwoFactorAuth\\TwoFactorException' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/TwoFactorException.php',
'OCP\\Authentication\\TwoFactorAuth\\TwoFactorProviderChallengeFailed' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailed.php',

View file

@ -14,6 +14,7 @@ use OCP\Authentication\TwoFactorAuth\IActivatableByAdmin;
use OCP\Authentication\TwoFactorAuth\IDeactivatableByAdmin;
use OCP\Authentication\TwoFactorAuth\IProvider;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Authentication\TwoFactorAuth\IStatelessProvider;
use OCP\IUser;
class ProviderManager {
@ -43,7 +44,9 @@ class ProviderManager {
public function tryEnableProviderFor(string $providerId, IUser $user): bool {
$provider = $this->getProvider($providerId, $user);
if ($provider instanceof IActivatableByAdmin) {
if ($provider instanceof IActivatableByAdmin
&& !($provider instanceof IStatelessProvider)
) {
$provider->enableFor($user);
$this->providerRegistry->enableProviderFor($provider, $user);
return true;
@ -62,7 +65,9 @@ class ProviderManager {
public function tryDisableProviderFor(string $providerId, IUser $user): bool {
$provider = $this->getProvider($providerId, $user);
if ($provider instanceof IDeactivatableByAdmin) {
if ($provider instanceof IDeactivatableByAdmin
&& !($provider instanceof IStatelessProvider)
) {
$provider->disableFor($user);
$this->providerRegistry->disableProviderFor($provider, $user);
return true;

View file

@ -12,6 +12,7 @@ namespace OC\Authentication\TwoFactorAuth;
use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao;
use OCP\Authentication\TwoFactorAuth\IProvider;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Authentication\TwoFactorAuth\IStatelessProvider;
use OCP\Authentication\TwoFactorAuth\RegistryEvent;
use OCP\Authentication\TwoFactorAuth\TwoFactorProviderDisabled;
use OCP\Authentication\TwoFactorAuth\TwoFactorProviderForUserRegistered;
@ -34,6 +35,10 @@ class Registry implements IRegistry {
#[\Override]
public function enableProviderFor(IProvider $provider, IUser $user) {
if ($provider instanceof IStatelessProvider) {
return;
}
$this->assignmentDao->persist($provider->getId(), $user->getUID(), 1);
$event = new RegistryEvent($provider, $user);
@ -43,6 +48,10 @@ class Registry implements IRegistry {
#[\Override]
public function disableProviderFor(IProvider $provider, IUser $user) {
if ($provider instanceof IStatelessProvider) {
return;
}
$this->assignmentDao->persist($provider->getId(), $user->getUID(), 0);
$event = new RegistryEvent($provider, $user);

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Authentication\TwoFactorAuth;
use OCP\AppFramework\Attribute\Implementable;
/**
* Marks the 2FA provider stateless. That means the state of 2FA activation
* for user will be checked dynamically and not stored in the database.
*
* @since 35.0.0
*/
#[Implementable(since: '35.0.0')]
interface IStatelessProvider extends IProvider {
}

View file

@ -13,6 +13,7 @@ use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao;
use OC\Authentication\TwoFactorAuth\Registry;
use OCP\Authentication\TwoFactorAuth\IProvider;
use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Authentication\TwoFactorAuth\IStatelessProvider;
use OCP\Authentication\TwoFactorAuth\RegistryEvent;
use OCP\Authentication\TwoFactorAuth\TwoFactorProviderDisabled;
use OCP\Authentication\TwoFactorAuth\TwoFactorProviderForUserRegistered;
@ -77,6 +78,18 @@ class RegistryTest extends TestCase {
$this->registry->enableProviderFor($provider, $user);
}
public function testEnableStatelessProvider(): void {
$user = $this->createMock(IUser::class);
$provider = $this->createMock(IStatelessProvider::class);
$this->dao->expects($this->never())->method('persist');
$this->dispatcher->expects($this->never())->method('dispatch');
$this->dispatcher->expects($this->never())->method('dispatchTyped');
$this->registry->enableProviderFor($provider, $user);
}
public function testDisableProvider(): void {
$user = $this->createMock(IUser::class);
$provider = $this->createMock(IProvider::class);
@ -103,6 +116,18 @@ class RegistryTest extends TestCase {
$this->registry->disableProviderFor($provider, $user);
}
public function testDisableStatelessProvider(): void {
$user = $this->createMock(IUser::class);
$provider = $this->createMock(IStatelessProvider::class);
$this->dao->expects($this->never())->method('persist');
$this->dispatcher->expects($this->never())->method('dispatch');
$this->dispatcher->expects($this->never())->method('dispatchTyped');
$this->registry->disableProviderFor($provider, $user);
}
public function testDeleteUserData(): void {
$user = $this->createMock(IUser::class);
$user->expects($this->once())->method('getUID')->willReturn('user123');