Theme System: Added AUTH_PRE_REGISTER logical event

Included tests to cover.
Manually tested on standard and social (GitHub) auth.
For #4833
This commit is contained in:
Dan Brown 2024-02-21 15:30:29 +00:00
parent be3423a16e
commit 055bbf17de
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
4 changed files with 82 additions and 25 deletions

View File

@ -14,20 +14,14 @@ use Illuminate\Support\Str;
class RegistrationService class RegistrationService
{ {
protected $userRepo; public function __construct(
protected $emailConfirmationService; protected UserRepo $userRepo,
protected EmailConfirmationService $emailConfirmationService,
/** ) {
* RegistrationService constructor.
*/
public function __construct(UserRepo $userRepo, EmailConfirmationService $emailConfirmationService)
{
$this->userRepo = $userRepo;
$this->emailConfirmationService = $emailConfirmationService;
} }
/** /**
* Check whether or not registrations are allowed in the app settings. * Check if registrations are allowed in the app settings.
* *
* @throws UserRegistrationException * @throws UserRegistrationException
*/ */
@ -84,6 +78,7 @@ class RegistrationService
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User
{ {
$userEmail = $userData['email']; $userEmail = $userData['email'];
$authSystem = $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver();
// Email restriction // Email restriction
$this->ensureEmailDomainAllowed($userEmail); $this->ensureEmailDomainAllowed($userEmail);
@ -94,6 +89,12 @@ class RegistrationService
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login'); throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login');
} }
/** @var ?bool $shouldRegister */
$shouldRegister = Theme::dispatch(ThemeEvents::AUTH_PRE_REGISTER, $authSystem, $userData);
if ($shouldRegister === false) {
throw new UserRegistrationException(trans('errors.auth_pre_register_theme_prevention'), '/login');
}
// Create the user // Create the user
$newUser = $this->userRepo->createWithoutActivity($userData, $emailConfirmed); $newUser = $this->userRepo->createWithoutActivity($userData, $emailConfirmed);
$newUser->attachDefaultRole(); $newUser->attachDefaultRole();
@ -104,7 +105,7 @@ class RegistrationService
} }
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser); Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
Theme::dispatch(ThemeEvents::AUTH_REGISTER, $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver(), $newUser); Theme::dispatch(ThemeEvents::AUTH_REGISTER, $authSystem, $newUser);
// Start email confirmation flow if required // Start email confirmation flow if required
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) { if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
@ -138,7 +139,7 @@ class RegistrationService
} }
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict)); $restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, '@'), 1); $userEmailDomain = mb_substr(mb_strrchr($userEmail, '@'), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) { if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
$redirect = $this->registrationAllowed() ? '/register' : '/login'; $redirect = $this->registrationAllowed() ? '/register' : '/login';

View File

@ -47,11 +47,30 @@ class ThemeEvents
*/ */
const AUTH_LOGIN = 'auth_login'; const AUTH_LOGIN = 'auth_login';
/**
* Auth pre-register event.
* Runs right before a new user account is registered in the system by any authentication
* system as a standard app user including auto-registration systems used by LDAP,
* SAML, OIDC and social systems. It only includes self-registrations,
* not accounts created by others in the UI or via the REST API.
* It runs after any other normal validation steps.
* Any account/email confirmation occurs post-registration.
* The provided $userData contains the main details that would be used to create
* the account, and may depend on authentication method.
* If false is returned from the event, registration will be prevented and the user
* will be returned to the login page.
*
* @param string $authSystem
* @param array $userData
* @returns bool|null
*/
const AUTH_PRE_REGISTER = 'auth_pre_register';
/** /**
* Auth register event. * Auth register event.
* Runs right after a user is newly registered to the application by any authentication * Runs right after a user is newly registered to the application by any authentication
* system as a standard app user. This includes auto-registration systems used * system as a standard app user. This includes auto-registration systems used
* by LDAP, SAML and social systems. It only includes self-registrations. * by LDAP, SAML, OIDC and social systems. It only includes self-registrations.
* *
* @param string $authSystem * @param string $authSystem
* @param \BookStack\Users\Models\User $user * @param \BookStack\Users\Models\User $user

View File

@ -10,6 +10,7 @@ return [
// Auth // Auth
'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.', 'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',
'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',
'email_already_confirmed' => 'Email has already been confirmed, Try logging in.', 'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',
'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.', 'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',
'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.', 'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',
@ -23,7 +24,6 @@ return [
'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.', 'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',
'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization', 'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'oidc_already_logged_in' => 'Already logged in', 'oidc_already_logged_in' => 'Already logged in',
'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system', 'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization', 'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
'social_no_action_defined' => 'No action defined', 'social_no_action_defined' => 'No action defined',

View File

@ -178,6 +178,43 @@ class ThemeTest extends TestCase
$this->assertInstanceOf(User::class, $args[1]); $this->assertInstanceOf(User::class, $args[1]);
} }
public function test_event_auth_pre_register()
{
$args = [];
$callback = function (...$eventArgs) use (&$args) {
$args = $eventArgs;
};
Theme::listen(ThemeEvents::AUTH_PRE_REGISTER, $callback);
$this->setSettings(['registration-enabled' => 'true']);
$user = User::factory()->make();
$this->post('/register', ['email' => $user->email, 'name' => $user->name, 'password' => 'password']);
$this->assertCount(2, $args);
$this->assertEquals('standard', $args[0]);
$this->assertEquals([
'email' => $user->email,
'name' => $user->name,
'password' => 'password',
], $args[1]);
$this->assertDatabaseHas('users', ['email' => $user->email]);
}
public function test_event_auth_pre_register_with_false_return_blocks_registration()
{
$callback = function () {
return false;
};
Theme::listen(ThemeEvents::AUTH_PRE_REGISTER, $callback);
$this->setSettings(['registration-enabled' => 'true']);
$user = User::factory()->make();
$resp = $this->post('/register', ['email' => $user->email, 'name' => $user->name, 'password' => 'password']);
$resp->assertRedirect('/login');
$this->assertSessionError('User account could not be registered for the provided details');
$this->assertDatabaseMissing('users', ['email' => $user->email]);
}
public function test_event_webhook_call_before() public function test_event_webhook_call_before()
{ {
$args = []; $args = [];