feat (2fa): Add IStatelessProvider interface

Signed-off-by: michal.roszak@put.poznan.pl <michal.roszak@put.poznan.pl>
This commit is contained in:
michal.roszak@put.poznan.pl 2026-05-05 10:39:25 +02:00 committed by Anna
parent f2ea807be2
commit c42bc0cf09
7 changed files with 66 additions and 2 deletions

View file

@ -629,6 +629,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

@ -183,6 +183,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

@ -224,6 +224,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

@ -13,6 +13,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 {
@ -42,7 +43,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;
@ -61,7 +64,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

@ -11,6 +11,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;
@ -33,6 +34,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);
@ -42,6 +47,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 34.0.0
*/
#[Implementable(since: '34.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);
@ -104,6 +117,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');