mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Checked over and aligned registration option behavior across all auth options
- Added tests to cover
This commit is contained in:
parent
e6c6de0848
commit
3991fbe726
@ -64,10 +64,8 @@ class ExternalAuthService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync the groups to the user roles for the current user
|
* Sync the groups to the user roles for the current user
|
||||||
* @param \BookStack\Auth\User $user
|
|
||||||
* @param array $userGroups
|
|
||||||
*/
|
*/
|
||||||
public function syncWithGroups(User $user, array $userGroups)
|
public function syncWithGroups(User $user, array $userGroups): void
|
||||||
{
|
{
|
||||||
// Get the ids for the roles from the names
|
// Get the ids for the roles from the names
|
||||||
$groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups);
|
$groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups);
|
||||||
@ -75,7 +73,7 @@ class ExternalAuthService
|
|||||||
// Sync groups
|
// Sync groups
|
||||||
if ($this->config['remove_from_groups']) {
|
if ($this->config['remove_from_groups']) {
|
||||||
$user->roles()->sync($groupsAsRoles);
|
$user->roles()->sync($groupsAsRoles);
|
||||||
$this->userRepo->attachDefaultRole($user);
|
$user->attachDefaultRole();
|
||||||
} else {
|
} else {
|
||||||
$user->roles()->syncWithoutDetaching($groupsAsRoles);
|
$user->roles()->syncWithoutDetaching($groupsAsRoles);
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Auth\Access\Guards;
|
namespace BookStack\Auth\Access\Guards;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\Access\RegistrationService;
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
|
||||||
use Illuminate\Auth\GuardHelpers;
|
use Illuminate\Auth\GuardHelpers;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||||
@ -56,23 +53,23 @@ class ExternalBaseSessionGuard implements StatefulGuard
|
|||||||
protected $loggedOut = false;
|
protected $loggedOut = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repository to perform user-specific actions.
|
* Service to handle common registration actions.
|
||||||
*
|
*
|
||||||
* @var UserRepo
|
* @var RegistrationService
|
||||||
*/
|
*/
|
||||||
protected $userRepo;
|
protected $registrationService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new authentication guard.
|
* Create a new authentication guard.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function __construct(string $name, UserProvider $provider, Session $session, UserRepo $userRepo)
|
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->session = $session;
|
$this->session = $session;
|
||||||
$this->provider = $provider;
|
$this->provider = $provider;
|
||||||
$this->userRepo = $userRepo;
|
$this->registrationService = $registrationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,13 +3,17 @@
|
|||||||
namespace BookStack\Auth\Access\Guards;
|
namespace BookStack\Auth\Access\Guards;
|
||||||
|
|
||||||
use BookStack\Auth\Access\LdapService;
|
use BookStack\Auth\Access\LdapService;
|
||||||
|
use BookStack\Auth\Access\RegistrationService;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use BookStack\Exceptions\LdapException;
|
use BookStack\Exceptions\LdapException;
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||||
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
use Illuminate\Contracts\Session\Session;
|
use Illuminate\Contracts\Session\Session;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class LdapSessionGuard extends ExternalBaseSessionGuard
|
class LdapSessionGuard extends ExternalBaseSessionGuard
|
||||||
{
|
{
|
||||||
@ -23,11 +27,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
UserProvider $provider,
|
UserProvider $provider,
|
||||||
Session $session,
|
Session $session,
|
||||||
LdapService $ldapService,
|
LdapService $ldapService,
|
||||||
UserRepo $userRepo
|
RegistrationService $registrationService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
$this->ldapService = $ldapService;
|
$this->ldapService = $ldapService;
|
||||||
parent::__construct($name, $provider, $session, $userRepo);
|
parent::__construct($name, $provider, $session, $registrationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -56,6 +60,7 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
* @throws LoginAttemptEmailNeededException
|
* @throws LoginAttemptEmailNeededException
|
||||||
* @throws LoginAttemptException
|
* @throws LoginAttemptException
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function attempt(array $credentials = [], $remember = false)
|
public function attempt(array $credentials = [], $remember = false)
|
||||||
{
|
{
|
||||||
@ -70,12 +75,9 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (is_null($user)) {
|
if (is_null($user)) {
|
||||||
$user = $this->freshUserInstanceFromLdapUserDetails($userDetails);
|
$user = $this->createNewFromLdapAndCreds($userDetails, $credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkForUserEmail($user, $credentials['email'] ?? '');
|
|
||||||
$this->saveIfNew($user);
|
|
||||||
|
|
||||||
// Sync LDAP groups if required
|
// Sync LDAP groups if required
|
||||||
if ($this->ldapService->shouldSyncGroups()) {
|
if ($this->ldapService->shouldSyncGroups()) {
|
||||||
$this->ldapService->syncGroups($user, $username);
|
$this->ldapService->syncGroups($user, $username);
|
||||||
@ -86,58 +88,27 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save the give user if they don't yet existing in the system.
|
* Create a new user from the given ldap credentials and login credentials
|
||||||
* @throws LoginAttemptException
|
|
||||||
*/
|
|
||||||
protected function saveIfNew(User $user)
|
|
||||||
{
|
|
||||||
if ($user->exists) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing users with same email
|
|
||||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
|
||||||
if ($alreadyUser) {
|
|
||||||
throw new LoginAttemptException(trans('errors.error_user_exists_different_creds', ['email' => $user->email]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->save();
|
|
||||||
$this->userRepo->attachDefaultRole($user);
|
|
||||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure the given user has an email.
|
|
||||||
* Takes the provided email in the request if a value is provided
|
|
||||||
* and the user does not have an existing email.
|
|
||||||
* @throws LoginAttemptEmailNeededException
|
* @throws LoginAttemptEmailNeededException
|
||||||
|
* @throws LoginAttemptException
|
||||||
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
protected function checkForUserEmail(User $user, string $providedEmail)
|
protected function createNewFromLdapAndCreds(array $ldapUserDetails, array $credentials): User
|
||||||
{
|
{
|
||||||
// Request email if missing from user and missing from request
|
$email = trim($ldapUserDetails['email'] ?: ($credentials['email'] ?? ''));
|
||||||
if (is_null($user->email) && !$providedEmail) {
|
|
||||||
|
if (empty($email)) {
|
||||||
throw new LoginAttemptEmailNeededException();
|
throw new LoginAttemptEmailNeededException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add email to model if non-existing and email provided in request
|
$details = [
|
||||||
if (!$user->exists && is_null($user->email) && $providedEmail) {
|
'name' => $ldapUserDetails['name'],
|
||||||
$user->email = $providedEmail;
|
'email' => $ldapUserDetails['email'] ?: $credentials['email'],
|
||||||
}
|
'external_auth_id' => $ldapUserDetails['uid'],
|
||||||
}
|
'password' => Str::random(32),
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
return $this->registrationService->registerUser($details, null, false);
|
||||||
* Create a fresh user instance from details provided by a LDAP lookup.
|
|
||||||
*/
|
|
||||||
protected function freshUserInstanceFromLdapUserDetails(array $ldapUserDetails): User
|
|
||||||
{
|
|
||||||
$user = new User();
|
|
||||||
|
|
||||||
$user->name = $ldapUserDetails['name'];
|
|
||||||
$user->external_auth_id = $ldapUserDetails['uid'];
|
|
||||||
$user->email = $ldapUserDetails['email'];
|
|
||||||
$user->email_confirmed = false;
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
<?php namespace BookStack\Auth\Access;
|
<?php namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\LdapException;
|
use BookStack\Exceptions\LdapException;
|
||||||
use ErrorException;
|
use ErrorException;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class LdapService
|
* Class LdapService
|
||||||
@ -16,17 +14,15 @@ class LdapService extends ExternalAuthService
|
|||||||
protected $ldap;
|
protected $ldap;
|
||||||
protected $ldapConnection;
|
protected $ldapConnection;
|
||||||
protected $config;
|
protected $config;
|
||||||
protected $userRepo;
|
|
||||||
protected $enabled;
|
protected $enabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LdapService constructor.
|
* LdapService constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(Ldap $ldap, UserRepo $userRepo)
|
public function __construct(Ldap $ldap)
|
||||||
{
|
{
|
||||||
$this->ldap = $ldap;
|
$this->ldap = $ldap;
|
||||||
$this->config = config('services.ldap');
|
$this->config = config('services.ldap');
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
$this->enabled = config('auth.method') === 'ldap';
|
$this->enabled = config('auth.method') === 'ldap';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php namespace BookStack\Auth\Access;
|
<?php namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\SocialAccount;
|
use BookStack\Auth\SocialAccount;
|
||||||
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use Exception;
|
use Exception;
|
||||||
@ -24,38 +25,51 @@ class RegistrationService
|
|||||||
* Check whether or not registrations are allowed in the app settings.
|
* Check whether or not registrations are allowed in the app settings.
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function checkRegistrationAllowed()
|
public function ensureRegistrationAllowed()
|
||||||
|
{
|
||||||
|
if (!$this->registrationAllowed()) {
|
||||||
|
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if standard BookStack User registrations are currently allowed.
|
||||||
|
* Does not prevent external-auth based registration.
|
||||||
|
*/
|
||||||
|
protected function registrationAllowed(): bool
|
||||||
{
|
{
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$authMethodsWithRegistration = ['standard'];
|
$authMethodsWithRegistration = ['standard'];
|
||||||
if (!setting('registration-enabled') || !in_array($authMethod, $authMethodsWithRegistration)) {
|
return in_array($authMethod, $authMethodsWithRegistration) && setting('registration-enabled');
|
||||||
throw new UserRegistrationException(trans('auth.registrations_disabled'), '/login');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The registrations flow for all users.
|
* The registrations flow for all users.
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailVerified = false)
|
public function registerUser(array $userData, ?SocialAccount $socialAccount = null, bool $emailConfirmed = false): User
|
||||||
{
|
{
|
||||||
$registrationRestrict = setting('registration-restrict');
|
$userEmail = $userData['email'];
|
||||||
|
|
||||||
if ($registrationRestrict) {
|
// Email restriction
|
||||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
$this->ensureEmailDomainAllowed($userEmail);
|
||||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
|
|
||||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
// Ensure user does not already exist
|
||||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
$alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));
|
||||||
}
|
if ($alreadyUser) {
|
||||||
|
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]));
|
||||||
}
|
}
|
||||||
|
|
||||||
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
|
// Create the user
|
||||||
|
$newUser = $this->userRepo->registerNew($userData, $emailConfirmed);
|
||||||
|
|
||||||
|
// Assign social account if given
|
||||||
if ($socialAccount) {
|
if ($socialAccount) {
|
||||||
$newUser->socialAccounts()->save($socialAccount);
|
$newUser->socialAccounts()->save($socialAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailVerified) {
|
// Start email confirmation flow if required
|
||||||
|
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||||
$newUser->save();
|
$newUser->save();
|
||||||
$message = '';
|
$message = '';
|
||||||
|
|
||||||
@ -68,7 +82,37 @@ class RegistrationService
|
|||||||
throw new UserRegistrationException($message, '/register/confirm');
|
throw new UserRegistrationException($message, '/register/confirm');
|
||||||
}
|
}
|
||||||
|
|
||||||
auth()->login($newUser);
|
return $newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the given email meets any active email domain registration restrictions.
|
||||||
|
* Throws if restrictions are active and the email does not match an allowed domain.
|
||||||
|
* @throws UserRegistrationException
|
||||||
|
*/
|
||||||
|
protected function ensureEmailDomainAllowed(string $userEmail): void
|
||||||
|
{
|
||||||
|
$registrationRestrict = setting('registration-restrict');
|
||||||
|
|
||||||
|
if (!$registrationRestrict) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||||
|
$userEmailDomain = $domain = mb_substr(mb_strrchr($userEmail, "@"), 1);
|
||||||
|
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||||
|
$redirect = $this->registrationAllowed() ? '/register' : '/login';
|
||||||
|
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias to the UserRepo method of the same name.
|
||||||
|
* Attaches the default system role, if configured, to the given user.
|
||||||
|
*/
|
||||||
|
public function attachDefaultRole(User $user): void
|
||||||
|
{
|
||||||
|
$this->userRepo->attachDefaultRole($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
<?php namespace BookStack\Auth\Access;
|
<?php namespace BookStack\Auth\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\JsonDebugException;
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
use BookStack\Exceptions\SamlException;
|
use BookStack\Exceptions\SamlException;
|
||||||
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use OneLogin\Saml2\Auth;
|
use OneLogin\Saml2\Auth;
|
||||||
@ -18,16 +18,16 @@ use OneLogin\Saml2\ValidationError;
|
|||||||
class Saml2Service extends ExternalAuthService
|
class Saml2Service extends ExternalAuthService
|
||||||
{
|
{
|
||||||
protected $config;
|
protected $config;
|
||||||
protected $userRepo;
|
protected $registrationService;
|
||||||
protected $user;
|
protected $user;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saml2Service constructor.
|
* Saml2Service constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(UserRepo $userRepo, User $user)
|
public function __construct(RegistrationService $registrationService, User $user)
|
||||||
{
|
{
|
||||||
$this->config = config('saml2');
|
$this->config = config('saml2');
|
||||||
$this->userRepo = $userRepo;
|
$this->registrationService = $registrationService;
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,6 +78,7 @@ class Saml2Service extends ExternalAuthService
|
|||||||
* @throws SamlException
|
* @throws SamlException
|
||||||
* @throws ValidationError
|
* @throws ValidationError
|
||||||
* @throws JsonDebugException
|
* @throws JsonDebugException
|
||||||
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function processAcsResponse(?string $requestId): ?User
|
public function processAcsResponse(?string $requestId): ?User
|
||||||
{
|
{
|
||||||
@ -308,34 +309,10 @@ class Saml2Service extends ExternalAuthService
|
|||||||
return $defaultValue;
|
return $defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a user that is authenticated but not already registered.
|
|
||||||
*/
|
|
||||||
protected function registerUser(array $userDetails): User
|
|
||||||
{
|
|
||||||
// Create an array of the user data to create a new user instance
|
|
||||||
$userData = [
|
|
||||||
'name' => $userDetails['name'],
|
|
||||||
'email' => $userDetails['email'],
|
|
||||||
'password' => Str::random(32),
|
|
||||||
'external_auth_id' => $userDetails['external_id'],
|
|
||||||
'email_confirmed' => true,
|
|
||||||
];
|
|
||||||
|
|
||||||
$existingUser = $this->user->newQuery()->where('email', '=', $userDetails['email'])->first();
|
|
||||||
if ($existingUser) {
|
|
||||||
throw new SamlException(trans('errors.saml_email_exists', ['email' => $userDetails['email']]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->user->newQuery()->forceCreate($userData);
|
|
||||||
$this->userRepo->attachDefaultRole($user);
|
|
||||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user from the database for the specified details.
|
* Get the user from the database for the specified details.
|
||||||
* @throws SamlException
|
* @throws SamlException
|
||||||
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
protected function getOrRegisterUser(array $userDetails): ?User
|
protected function getOrRegisterUser(array $userDetails): ?User
|
||||||
{
|
{
|
||||||
@ -344,7 +321,14 @@ class Saml2Service extends ExternalAuthService
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (is_null($user)) {
|
if (is_null($user)) {
|
||||||
$user = $this->registerUser($userDetails);
|
$userData = [
|
||||||
|
'name' => $userDetails['name'],
|
||||||
|
'email' => $userDetails['email'],
|
||||||
|
'password' => Str::random(32),
|
||||||
|
'external_auth_id' => $userDetails['external_id'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = $this->registrationService->registerUser($userData, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
@ -355,6 +339,7 @@ class Saml2Service extends ExternalAuthService
|
|||||||
* they exist, optionally registering them automatically.
|
* they exist, optionally registering them automatically.
|
||||||
* @throws SamlException
|
* @throws SamlException
|
||||||
* @throws JsonDebugException
|
* @throws JsonDebugException
|
||||||
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
public function processLoginCallback(string $samlID, array $samlAttributes): User
|
public function processLoginCallback(string $samlID, array $samlAttributes): User
|
||||||
{
|
{
|
||||||
|
@ -64,7 +64,7 @@ class SocialAuthService
|
|||||||
|
|
||||||
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
|
||||||
$email = $socialUser->getEmail();
|
$email = $socialUser->getEmail();
|
||||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver, 'email' => $email]), '/login');
|
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $socialUser;
|
return $socialUser;
|
||||||
@ -124,7 +124,7 @@ class SocialAuthService
|
|||||||
|
|
||||||
// Otherwise let the user know this social account is not used by anyone.
|
// Otherwise let the user know this social account is not used by anyone.
|
||||||
$message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
|
$message = trans('errors.social_account_not_used', ['socialAccount' => $titleCaseDriver]);
|
||||||
if (setting('registration-enabled') && config('auth.method') !== 'ldap') {
|
if (setting('registration-enabled') && config('auth.method') !== 'ldap' && config('auth.method') !== 'saml2') {
|
||||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
|
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +116,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||||||
return $this->roles->pluck('system_name')->contains($role);
|
return $this->roles->pluck('system_name')->contains($role);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach the default system role to this user.
|
||||||
|
*/
|
||||||
|
public function attachDefaultRole(): void
|
||||||
|
{
|
||||||
|
$roleId = setting('registration-role');
|
||||||
|
if ($roleId && $this->roles()->where('id', '=', $roleId)->count() === 0) {
|
||||||
|
$this->roles()->attach($roleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all permissions belonging to a the current user.
|
* Get all permissions belonging to a the current user.
|
||||||
* @param bool $cache
|
* @param bool $cache
|
||||||
@ -153,16 +164,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||||||
*/
|
*/
|
||||||
public function attachRole(Role $role)
|
public function attachRole(Role $role)
|
||||||
{
|
{
|
||||||
$this->attachRoleId($role->id);
|
$this->roles()->attach($role->id);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attach a role id to this user.
|
|
||||||
* @param $id
|
|
||||||
*/
|
|
||||||
public function attachRoleId($id)
|
|
||||||
{
|
|
||||||
$this->roles()->attach($id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,10 +29,9 @@ class UserRepo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $email
|
* Get a user by their email address.
|
||||||
* @return User|null
|
|
||||||
*/
|
*/
|
||||||
public function getByEmail($email)
|
public function getByEmail(string $email): ?User
|
||||||
{
|
{
|
||||||
return $this->user->where('email', '=', $email)->first();
|
return $this->user->where('email', '=', $email)->first();
|
||||||
}
|
}
|
||||||
@ -78,31 +77,16 @@ class UserRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new user and attaches a role to them.
|
* Creates a new user and attaches a role to them.
|
||||||
* @param array $data
|
|
||||||
* @param boolean $verifyEmail
|
|
||||||
* @return User
|
|
||||||
*/
|
*/
|
||||||
public function registerNew(array $data, $verifyEmail = false)
|
public function registerNew(array $data, bool $emailConfirmed = false): User
|
||||||
{
|
{
|
||||||
$user = $this->create($data, $verifyEmail);
|
$user = $this->create($data, $emailConfirmed);
|
||||||
$this->attachDefaultRole($user);
|
$user->attachDefaultRole();
|
||||||
$this->downloadAndAssignUserAvatar($user);
|
$this->downloadAndAssignUserAvatar($user);
|
||||||
|
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Give a user the default role. Used when creating a new user.
|
|
||||||
* @param User $user
|
|
||||||
*/
|
|
||||||
public function attachDefaultRole(User $user)
|
|
||||||
{
|
|
||||||
$roleId = setting('registration-role');
|
|
||||||
if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
|
|
||||||
$user->attachRoleId($roleId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign a user to a system-level role.
|
* Assign a user to a system-level role.
|
||||||
* @param User $user
|
* @param User $user
|
||||||
@ -172,17 +156,15 @@ class UserRepo
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new basic instance of user.
|
* Create a new basic instance of user.
|
||||||
* @param array $data
|
|
||||||
* @param boolean $verifyEmail
|
|
||||||
* @return User
|
|
||||||
*/
|
*/
|
||||||
public function create(array $data, $verifyEmail = false)
|
public function create(array $data, bool $emailConfirmed = false): User
|
||||||
{
|
{
|
||||||
return $this->user->forceCreate([
|
return $this->user->forceCreate([
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'email' => $data['email'],
|
'email' => $data['email'],
|
||||||
'password' => bcrypt($data['password']),
|
'password' => bcrypt($data['password']),
|
||||||
'email_confirmed' => $verifyEmail
|
'email_confirmed' => $emailConfirmed,
|
||||||
|
'external_auth_id' => $data['external_auth_id'] ?? '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,10 @@ class Handler extends ExceptionHandler
|
|||||||
// Handle notify exceptions which will redirect to the
|
// Handle notify exceptions which will redirect to the
|
||||||
// specified location then show a notification message.
|
// specified location then show a notification message.
|
||||||
if ($this->isExceptionType($e, NotifyException::class)) {
|
if ($this->isExceptionType($e, NotifyException::class)) {
|
||||||
session()->flash('error', $this->getOriginalMessage($e));
|
$message = $this->getOriginalMessage($e);
|
||||||
|
if (!empty($message)) {
|
||||||
|
session()->flash('error', $message);
|
||||||
|
}
|
||||||
return redirect($e->redirectLocation);
|
return redirect($e->redirectLocation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
|
|||||||
use BookStack\Auth\Access\SocialAuthService;
|
use BookStack\Auth\Access\SocialAuthService;
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
@ -74,7 +74,7 @@ class RegisterController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getRegister()
|
public function getRegister()
|
||||||
{
|
{
|
||||||
$this->registrationService->checkRegistrationAllowed();
|
$this->registrationService->ensureRegistrationAllowed();
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||||
return view('auth.register', [
|
return view('auth.register', [
|
||||||
'socialDrivers' => $socialDrivers,
|
'socialDrivers' => $socialDrivers,
|
||||||
@ -87,12 +87,13 @@ class RegisterController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function postRegister(Request $request)
|
public function postRegister(Request $request)
|
||||||
{
|
{
|
||||||
$this->registrationService->checkRegistrationAllowed();
|
$this->registrationService->ensureRegistrationAllowed();
|
||||||
$this->validator($request->all())->validate();
|
$this->validator($request->all())->validate();
|
||||||
$userData = $request->all();
|
$userData = $request->all();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->registrationService->registerUser($userData);
|
$user = $this->registrationService->registerUser($userData);
|
||||||
|
auth()->login($user);
|
||||||
} catch (UserRegistrationException $exception) {
|
} catch (UserRegistrationException $exception) {
|
||||||
if ($exception->getMessage()) {
|
if ($exception->getMessage()) {
|
||||||
$this->showErrorNotification($exception->getMessage());
|
$this->showErrorNotification($exception->getMessage());
|
||||||
|
@ -81,7 +81,6 @@ class Saml2Controller extends Controller
|
|||||||
return redirect('/login');
|
return redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->put('last_login_type', 'saml2');
|
|
||||||
return redirect()->intended();
|
return redirect()->intended();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class SocialController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function socialRegister(string $socialDriver)
|
public function socialRegister(string $socialDriver)
|
||||||
{
|
{
|
||||||
$this->registrationService->checkRegistrationAllowed();
|
$this->registrationService->ensureRegistrationAllowed();
|
||||||
session()->put('social-callback', 'register');
|
session()->put('social-callback', 'register');
|
||||||
return $this->socialAuthService->startRegister($socialDriver);
|
return $this->socialAuthService->startRegister($socialDriver);
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ class SocialController extends Controller
|
|||||||
|
|
||||||
// Attempt login or fall-back to register if allowed.
|
// Attempt login or fall-back to register if allowed.
|
||||||
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
|
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
|
||||||
if ($action == 'login') {
|
if ($action === 'login') {
|
||||||
try {
|
try {
|
||||||
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
||||||
} catch (SocialSignInAccountNotUsed $exception) {
|
} catch (SocialSignInAccountNotUsed $exception) {
|
||||||
@ -89,7 +89,7 @@ class SocialController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($action == 'register') {
|
if ($action === 'register') {
|
||||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,6 @@ class SocialController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user after a registration callback.
|
* Register a new user after a registration callback.
|
||||||
* @return RedirectResponse|Redirector
|
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
||||||
@ -121,17 +120,11 @@ class SocialController extends Controller
|
|||||||
$userData = [
|
$userData = [
|
||||||
'name' => $socialUser->getName(),
|
'name' => $socialUser->getName(),
|
||||||
'email' => $socialUser->getEmail(),
|
'email' => $socialUser->getEmail(),
|
||||||
'password' => Str::random(30)
|
'password' => Str::random(32)
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
$user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
|
||||||
$this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
|
auth()->login($user);
|
||||||
} catch (UserRegistrationException $exception) {
|
|
||||||
if ($exception->getMessage()) {
|
|
||||||
$this->showErrorNotification($exception->getMessage());
|
|
||||||
}
|
|
||||||
return redirect($exception->redirectLocation);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->showSuccessNotification(trans('auth.register_success'));
|
$this->showSuccessNotification(trans('auth.register_success'));
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
|
@ -8,6 +8,7 @@ use BookStack\Auth\Access\ExternalBaseUserProvider;
|
|||||||
use BookStack\Auth\Access\Guards\LdapSessionGuard;
|
use BookStack\Auth\Access\Guards\LdapSessionGuard;
|
||||||
use BookStack\Auth\Access\Guards\Saml2SessionGuard;
|
use BookStack\Auth\Access\Guards\Saml2SessionGuard;
|
||||||
use BookStack\Auth\Access\LdapService;
|
use BookStack\Auth\Access\LdapService;
|
||||||
|
use BookStack\Auth\Access\RegistrationService;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
$provider,
|
$provider,
|
||||||
$this->app['session.store'],
|
$this->app['session.store'],
|
||||||
$app[LdapService::class],
|
$app[LdapService::class],
|
||||||
$app[UserRepo::class]
|
$app[RegistrationService::class]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
$name,
|
$name,
|
||||||
$provider,
|
$provider,
|
||||||
$this->app['session.store'],
|
$this->app['session.store'],
|
||||||
$app[UserRepo::class]
|
$app[RegistrationService::class]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ return [
|
|||||||
'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
|
||||||
'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',
|
||||||
'saml_email_exists' => 'Registration unsuccessful since a user already exists with email address ":email"',
|
|
||||||
'social_no_action_defined' => 'No action defined',
|
'social_no_action_defined' => 'No action defined',
|
||||||
'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
|
'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
|
||||||
'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
|
'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
|
||||||
|
@ -56,7 +56,7 @@ return [
|
|||||||
'reg_enable_toggle' => 'Enable registration',
|
'reg_enable_toggle' => 'Enable registration',
|
||||||
'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
|
'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
|
||||||
'reg_default_role' => 'Default user role after registration',
|
'reg_default_role' => 'Default user role after registration',
|
||||||
'reg_enable_ldap_warning' => 'The option above is not used while LDAP authentication is active. User accounts for non-existing members will be auto-created if authentication, against the LDAP system in use, is successful.',
|
'reg_enable_external_warning' => 'The option above is ignore while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',
|
||||||
'reg_email_confirmation' => 'Email Confirmation',
|
'reg_email_confirmation' => 'Email Confirmation',
|
||||||
'reg_email_confirmation_toggle' => 'Require email confirmation',
|
'reg_email_confirmation_toggle' => 'Require email confirmation',
|
||||||
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',
|
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',
|
||||||
|
@ -219,8 +219,8 @@
|
|||||||
'label' => trans('settings.reg_enable_toggle')
|
'label' => trans('settings.reg_enable_toggle')
|
||||||
])
|
])
|
||||||
|
|
||||||
@if(config('auth.method') === 'ldap')
|
@if(in_array(config('auth.method'), ['ldap', 'saml2']))
|
||||||
<div class="text-warn text-small mb-l">{{ trans('settings.reg_enable_ldap_warning') }}</div>
|
<div class="text-warn text-small mb-l">{{ trans('settings.reg_enable_external_warning') }}</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
|
<label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
|
||||||
|
@ -86,6 +86,38 @@ class LdapTest extends BrowserKitTest
|
|||||||
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
|
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_email_domain_restriction_active_on_new_ldap_login()
|
||||||
|
{
|
||||||
|
$this->setSettings([
|
||||||
|
'registration-restrict' => 'testing.com'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||||
|
$this->mockLdap->shouldReceive('setVersion')->once();
|
||||||
|
$this->mockLdap->shouldReceive('setOption')->times(2);
|
||||||
|
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
|
||||||
|
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
|
||||||
|
->andReturn(['count' => 1, 0 => [
|
||||||
|
'uid' => [$this->mockUser->name],
|
||||||
|
'cn' => [$this->mockUser->name],
|
||||||
|
'dn' => ['dc=test' . config('services.ldap.base_dn')]
|
||||||
|
]]);
|
||||||
|
$this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
|
||||||
|
$this->mockEscapes(2);
|
||||||
|
|
||||||
|
$this->mockUserLogin()
|
||||||
|
->seePageIs('/login')
|
||||||
|
->see('Please enter an email to use for this account.');
|
||||||
|
|
||||||
|
$email = 'tester@invaliddomain.com';
|
||||||
|
|
||||||
|
$this->type($email, '#email')
|
||||||
|
->press('Log In')
|
||||||
|
->seePageIs('/login')
|
||||||
|
->see('That email domain does not have access to this application')
|
||||||
|
->dontSeeInDatabase('users', ['email' => $email]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_login_works_when_no_uid_provided_by_ldap_server()
|
public function test_login_works_when_no_uid_provided_by_ldap_server()
|
||||||
{
|
{
|
||||||
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
|
||||||
|
@ -73,7 +73,7 @@ class Saml2Test extends TestCase
|
|||||||
$this->assertDatabaseHas('users', [
|
$this->assertDatabaseHas('users', [
|
||||||
'email' => 'user@example.com',
|
'email' => 'user@example.com',
|
||||||
'external_auth_id' => 'user',
|
'external_auth_id' => 'user',
|
||||||
'email_confirmed' => true,
|
'email_confirmed' => false,
|
||||||
'name' => 'Barry Scott'
|
'name' => 'Barry Scott'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ class Saml2Test extends TestCase
|
|||||||
$acsPost = $this->post('/saml2/acs');
|
$acsPost = $this->post('/saml2/acs');
|
||||||
$acsPost->assertRedirect('/');
|
$acsPost->assertRedirect('/');
|
||||||
$errorMessage = session()->get('error');
|
$errorMessage = session()->get('error');
|
||||||
$this->assertEquals('Registration unsuccessful since a user already exists with email address "user@example.com"', $errorMessage);
|
$this->assertEquals('A user with the email user@example.com already exists but with different credentials.', $errorMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,6 +271,24 @@ class Saml2Test extends TestCase
|
|||||||
$this->assertPermissionError($resp);
|
$this->assertPermissionError($resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_email_domain_restriction_active_on_new_saml_login()
|
||||||
|
{
|
||||||
|
$this->setSettings([
|
||||||
|
'registration-restrict' => 'testing.com'
|
||||||
|
]);
|
||||||
|
config()->set([
|
||||||
|
'saml2.onelogin.strict' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withPost(['SAMLResponse' => $this->acsPostData], function () {
|
||||||
|
$acsPost = $this->post('/saml2/acs');
|
||||||
|
$acsPost->assertRedirect('/login');
|
||||||
|
$errorMessage = session()->get('error');
|
||||||
|
$this->assertStringContainsString('That email domain does not have access to this application', $errorMessage);
|
||||||
|
$this->assertDatabaseMissing('users', ['email' => 'user@example.com']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected function withGet(array $options, callable $callback)
|
protected function withGet(array $options, callable $callback)
|
||||||
{
|
{
|
||||||
return $this->withGlobal($_GET, $options, $callback);
|
return $this->withGlobal($_GET, $options, $callback);
|
||||||
|
Loading…
Reference in New Issue
Block a user