From: Dan Brown Date: Wed, 6 Oct 2021 12:17:30 +0000 (+0100) Subject: Merge branch 'openid' of https://github.com/jasperweyne/BookStack into jasperweyne... X-Git-Tag: v21.10~1^2~21^2~11 X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/193d7fb3fe71a76a43ebc1ccdb617b4b627d1e09 Merge branch 'openid' of https://github.com/jasperweyne/BookStack into jasperweyne-openid --- 193d7fb3fe71a76a43ebc1ccdb617b4b627d1e09 diff --cc app/Auth/Access/ExternalAuthService.php index 7bd3679ac,7f15307ae..b0c9e8e7b --- a/app/Auth/Access/ExternalAuthService.php +++ b/app/Auth/Access/ExternalAuthService.php @@@ -4,10 -2,47 +4,48 @@@ namespace BookStack\Auth\Access use BookStack\Auth\Role; use BookStack\Auth\User; +use Illuminate\Support\Collection; + use Illuminate\Database\Eloquent\Builder; + use Illuminate\Support\Str; class ExternalAuthService { + protected $registrationService; + protected $user; + + /** + * ExternalAuthService base constructor. + */ + public function __construct(RegistrationService $registrationService, User $user) + { + $this->registrationService = $registrationService; + $this->user = $user; + } + + /** + * Get the user from the database for the specified details. + * @throws UserRegistrationException + */ + protected function getOrRegisterUser(array $userDetails): ?User + { - $user = $this->user->newQuery() ++ $user = User::query() + ->where('external_auth_id', '=', $userDetails['external_id']) + ->first(); + + if (is_null($user)) { + $userData = [ - 'name' => $userDetails['name'], - 'email' => $userDetails['email'], - 'password' => Str::random(32), ++ '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; + } + /** * Check a role against an array of group names to see if it matches. * Checked against role 'external_auth_id' if set otherwise the name of the role. diff --cc app/Auth/Access/LoginService.php index e02296b37,000000000..b36adb522 mode 100644,000000..100644 --- a/app/Auth/Access/LoginService.php +++ b/app/Auth/Access/LoginService.php @@@ -1,164 -1,0 +1,164 @@@ +mfaSession = $mfaSession; + $this->emailConfirmationService = $emailConfirmationService; + } + + /** + * Log the given user into the system. + * Will start a login of the given user but will prevent if there's + * a reason to (MFA or Unconfirmed Email). + * Returns a boolean to indicate the current login result. + * + * @throws StoppedAuthenticationException + */ + public function login(User $user, string $method, bool $remember = false): void + { + if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) { + $this->setLastLoginAttemptedForUser($user, $method, $remember); + + throw new StoppedAuthenticationException($user, $this); + } + + $this->clearLastLoginAttempted(); + auth()->login($user, $remember); + Activity::add(ActivityType::AUTH_LOGIN, "{$method}; {$user->logDescriptor()}"); + Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user); + + // Authenticate on all session guards if a likely admin + if ($user->can('users-manage') && $user->can('user-roles-manage')) { - $guards = ['standard', 'ldap', 'saml2']; ++ $guards = ['standard', 'ldap', 'saml2', 'openid']; + foreach ($guards as $guard) { + auth($guard)->login($user); + } + } + } + + /** + * Reattempt a system login after a previous stopped attempt. + * + * @throws Exception + */ + public function reattemptLoginFor(User $user) + { + if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) { + throw new Exception('Login reattempt user does align with current session state'); + } + + $lastLoginDetails = $this->getLastLoginAttemptDetails(); + $this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false); + } + + /** + * Get the last user that was attempted to be logged in. + * Only exists if the last login attempt had correct credentials + * but had been prevented by a secondary factor. + */ + public function getLastLoginAttemptUser(): ?User + { + $id = $this->getLastLoginAttemptDetails()['user_id']; + + return User::query()->where('id', '=', $id)->first(); + } + + /** + * Get the details of the last login attempt. + * Checks upon a ttl of about 1 hour since that last attempted login. + * + * @return array{user_id: ?string, method: ?string, remember: bool} + */ + protected function getLastLoginAttemptDetails(): array + { + $value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY); + if (!$value) { + return ['user_id' => null, 'method' => null]; + } + + [$id, $method, $remember, $time] = explode(':', $value); + $hourAgo = time() - (60 * 60); + if ($time < $hourAgo) { + $this->clearLastLoginAttempted(); + + return ['user_id' => null, 'method' => null]; + } + + return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)]; + } + + /** + * Set the last login attempted user. + * Must be only used when credentials are correct and a login could be + * achieved but a secondary factor has stopped the login. + */ + protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember) + { + session()->put( + self::LAST_LOGIN_ATTEMPTED_SESSION_KEY, + implode(':', [$user->id, $method, $remember, time()]) + ); + } + + /** + * Clear the last login attempted session value. + */ + protected function clearLastLoginAttempted(): void + { + session()->remove(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY); + } + + /** + * Check if MFA verification is needed. + */ + public function needsMfaVerification(User $user): bool + { + return !$this->mfaSession->isVerifiedForUser($user) && $this->mfaSession->isRequiredForUser($user); + } + + /** + * Check if the given user is awaiting email confirmation. + */ + public function awaitingEmailConfirmation(User $user): bool + { + return $this->emailConfirmationService->confirmationRequired() && !$user->email_confirmed; + } + + /** + * Attempt the login of a user using the given credentials. + * Meant to mirror Laravel's default guard 'attempt' method + * but in a manner that always routes through our login system. + * May interrupt the flow if extra authentication requirements are imposed. + * + * @throws StoppedAuthenticationException + */ + public function attempt(array $credentials, string $method, bool $remember = false): bool + { + $result = auth()->attempt($credentials, $remember); + if ($result) { + $user = auth()->user(); + auth()->logout(); + $this->login($user, $method, $remember); + } + + return $result; + } +} diff --cc app/Auth/Access/Saml2Service.php index 6cbfdac0b,4c1fce864..74e8c7726 --- a/app/Auth/Access/Saml2Service.php +++ b/app/Auth/Access/Saml2Service.php @@@ -5,10 -3,8 +5,9 @@@ namespace BookStack\Auth\Access use BookStack\Auth\User; use BookStack\Exceptions\JsonDebugException; use BookStack\Exceptions\SamlException; +use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Exceptions\UserRegistrationException; use Exception; - use Illuminate\Support\Str; use OneLogin\Saml2\Auth; use OneLogin\Saml2\Error; use OneLogin\Saml2\IdPMetadataParser; @@@ -27,11 -21,11 +26,13 @@@ class Saml2Service extends ExternalAuth /** * Saml2Service constructor. */ - public function __construct(RegistrationService $registrationService, LoginService $loginService) - public function __construct(RegistrationService $registrationService, User $user) ++ public function __construct(RegistrationService $registrationService, LoginService $loginService, User $user), { + parent::__construct($registrationService, $user); + $this->config = config('saml2'); + $this->registrationService = $registrationService; + $this->loginService = $loginService; } /** diff --cc app/Config/auth.php index 404b5352d,a1824bc78..5b39bafed --- a/app/Config/auth.php +++ b/app/Config/auth.php @@@ -37,9 -37,13 +37,13 @@@ return 'provider' => 'external', ], 'saml2' => [ - 'driver' => 'saml2-session', + 'driver' => 'saml2-session', 'provider' => 'external', ], + 'openid' => [ + 'driver' => 'openid-session', + 'provider' => 'external', + ], 'api' => [ 'driver' => 'api-token', ], diff --cc app/Http/Middleware/VerifyCsrfToken.php index 804a22bc0,007564eb3..a2e7f1dc1 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@@ -20,5 -20,6 +20,6 @@@ class VerifyCsrfToken extends Middlewar */ protected $except = [ 'saml2/*', - 'openid/*' ++ 'openid/*', ]; } diff --cc app/Providers/AuthServiceProvider.php index 37b0e83b9,653a29248..cd90cc849 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@@ -6,10 -7,11 +6,12 @@@ use BookStack\Api\ApiTokenGuard use BookStack\Auth\Access\ExternalBaseUserProvider; use BookStack\Auth\Access\Guards\LdapSessionGuard; use BookStack\Auth\Access\Guards\Saml2SessionGuard; + use BookStack\Auth\Access\Guards\OpenIdSessionGuard; use BookStack\Auth\Access\LdapService; +use BookStack\Auth\Access\LoginService; + use BookStack\Auth\Access\OpenIdService; use BookStack\Auth\Access\RegistrationService; -use BookStack\Auth\UserRepo; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; class AuthServiceProvider extends ServiceProvider diff --cc composer.json index 31ecbef84,7b1a3d592..288f55991 --- a/composer.json +++ b/composer.json @@@ -12,38 -11,39 +12,39 @@@ "ext-gd": "*", "ext-json": "*", "ext-mbstring": "*", - "ext-tidy": "*", "ext-xml": "*", - "barryvdh/laravel-dompdf": "^0.8.6", - "barryvdh/laravel-snappy": "^0.4.7", - "doctrine/dbal": "^2.9", - "facade/ignition": "^1.4", - "fideloper/proxy": "^4.0", - "gathercontent/htmldiff": "^0.2.1", - "intervention/image": "^2.5", - "laravel/framework": "^6.18", - "laravel/socialite": "^4.3.2", - "league/commonmark": "^1.4", - "league/flysystem-aws-s3-v3": "^1.0", - "nunomaduro/collision": "^3.0", - "onelogin/php-saml": "^3.3", - "predis/predis": "^1.1", - "socialiteproviders/discord": "^2.0", - "socialiteproviders/gitlab": "^3.0", - "socialiteproviders/microsoft-azure": "^3.0", - "socialiteproviders/okta": "^1.0", - "socialiteproviders/slack": "^3.0", - "socialiteproviders/twitch": "^5.0", + "bacon/bacon-qr-code": "^2.0", + "barryvdh/laravel-dompdf": "^0.9.0", + "barryvdh/laravel-snappy": "^0.4.8", + "doctrine/dbal": "^2.12.1", + "facade/ignition": "^1.16.4", + "fideloper/proxy": "^4.4.1", + "intervention/image": "^2.5.1", + "laravel/framework": "^6.20.33", + "laravel/socialite": "^5.1", + "league/commonmark": "^1.5", + "league/flysystem-aws-s3-v3": "^1.0.29", + "league/html-to-markdown": "^5.0.0", + "nunomaduro/collision": "^3.1", + "onelogin/php-saml": "^4.0", + "pragmarx/google2fa": "^8.0", + "predis/predis": "^1.1.6", + "socialiteproviders/discord": "^4.1", + "socialiteproviders/gitlab": "^4.1", + "socialiteproviders/microsoft-azure": "^4.1", + "socialiteproviders/okta": "^4.1", + "socialiteproviders/slack": "^4.1", + "socialiteproviders/twitch": "^5.3", - "ssddanbrown/htmldiff": "^v1.0.1" ++ "ssddanbrown/htmldiff": "^v1.0.1", + "steverhoades/oauth2-openid-connect-client": "^0.3.0" }, "require-dev": { - "barryvdh/laravel-debugbar": "^3.2.8", - "barryvdh/laravel-ide-helper": "^2.6.4", - "fzaninotto/faker": "^1.4", - "laravel/browser-kit-testing": "^5.1", - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^8.0", - "squizlabs/php_codesniffer": "^3.4", - "wnx/laravel-stats": "^2.0" + "barryvdh/laravel-debugbar": "^3.5.1", + "barryvdh/laravel-ide-helper": "^2.8.2", + "fakerphp/faker": "^1.13.0", + "mockery/mockery": "^1.3.3", + "phpunit/phpunit": "^9.5.3", + "symfony/dom-crawler": "^5.3" }, "autoload": { "classmap": [ diff --cc composer.lock index d267d13d6,0f5e29792..a3cfe6e7e --- a/composer.lock +++ b/composer.lock @@@ -2028,15 -1812,76 +2028,80 @@@ "laravel", "oauth" ], - "time": "2020-02-04T15:30:01+00:00" + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2021-08-31T15:16:26+00:00" }, + { + "name": "lcobucci/jwt", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "56f10808089e38623345e28af2f2d5e4eb579455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/56f10808089e38623345e28af2f2d5e4eb579455", + "reference": "56f10808089e38623345e28af2f2d5e4eb579455", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-openssl": "*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "mikey179/vfsstream": "~1.5", + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "^5.7 || ^7.3", + "squizlabs/php_codesniffer": "~2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Otávio Cobucci Oblonczyk", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2020-05-22T08:21:12+00:00" + }, { "name": "league/commonmark", - "version": "1.4.3", + "version": "1.6.6", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", @@@ -2495,15 -2177,78 +2560,82 @@@ "tumblr", "twitter" ], - "time": "2016-08-17T00:36:58+00:00" + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.0" + }, + "time": "2021-08-15T23:05:49+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "cc114abc622a53af969e8664722e84ca36257530" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/cc114abc622a53af969e8664722e84ca36257530", + "reference": "cc114abc622a53af969e8664722e84ca36257530", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0", + "paragonie/random_compat": "^1|^2|^9.99", + "php": "^5.6|^7.0" + }, + "require-dev": { + "eloquent/liberator": "^2.0", + "eloquent/phony-phpunit": "^1.0|^3.0", + "jakub-onderka/php-parallel-lint": "^0.9.2", + "phpunit/phpunit": "^5.7|^6.0", + "squizlabs/php_codesniffer": "^2.3|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "time": "2018-11-22T18:33:57+00:00" + }, { "name": "monolog/monolog", - "version": "2.1.0", + "version": "2.3.4", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", @@@ -4338,51 -3644,42 +4470,146 @@@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/steverhoades/oauth2-openid-connect-client/zipball/0159471487540a4620b8d0b693f5f215503a8d75", + "url": "https://api.github.com/repos/ssddanbrown/HtmlDiff/zipball/f60d5cc278b60305ab980a6665f46117c5b589c0", + "reference": "f60d5cc278b60305ab980a6665f46117c5b589c0", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.2" ++ }, ++ "require-dev": { ++ "phpunit/phpunit": "^8.5|^9.4.3" ++ }, ++ "type": "library", ++ "autoload": { ++ "psr-4": { ++ "Ssddanbrown\\HtmlDiff\\": "src" ++ } ++ }, ++ "notification-url": "https://packagist.org/downloads/", ++ "license": [ ++ "MIT" ++ ], ++ "authors": [ ++ { ++ "name": "Dan Brown", ++ "email": "ssddanbrown@googlemail.com", ++ "role": "Developer" ++ } ++ ], ++ "description": "HTML Content Diff Generator", ++ "homepage": "https://github.com/ssddanbrown/htmldiff", ++ "support": { ++ "issues": "https://github.com/ssddanbrown/HtmlDiff/issues", ++ "source": "https://github.com/ssddanbrown/HtmlDiff/tree/v1.0.1" ++ }, ++ "funding": [ ++ { ++ "url": "https://github.com/ssddanbrown", ++ "type": "github" ++ } ++ ], ++ "time": "2021-01-24T18:51:30+00:00" ++ }, ++ { ++ "name": "steverhoades/oauth2-openid-connect-client", ++ "version": "v0.3.0", ++ "source": { ++ "type": "git", ++ "url": "https://github.com/steverhoades/oauth2-openid-connect-client.git", ++ "reference": "0159471487540a4620b8d0b693f5f215503a8d75" ++ }, ++ "dist": { ++ "type": "zip", ++ "url": "https://api.github.com/repos/steverhoades/oauth2-openid-connect-client/zipball/0159471487540a4620b8d0b693f5f215503a8d75", + "reference": "0159471487540a4620b8d0b693f5f215503a8d75", + "shasum": "" + }, + "require": { + "lcobucci/jwt": "^3.2", + "league/oauth2-client": "^2.0" + }, + "require-dev": { + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "~4.5", + "squizlabs/php_codesniffer": "~2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "OpenIDConnectClient\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steve Rhoades", + "email": "sedonami@gmail.com" + } + ], + "description": "OAuth2 OpenID Connect Client that utilizes the PHP Leagues OAuth2 Client", + "time": "2020-05-19T23:06:36+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v6.2.3", ++ "source": { ++ "type": "git", ++ "url": "https://github.com/ssddanbrown/HtmlDiff.git", ++ "reference": "f60d5cc278b60305ab980a6665f46117c5b589c0" ++ }, ++ "dist": { ++ "type": "zip", ++ "url": "https://api.github.com/repos/ssddanbrown/HtmlDiff/zipball/f60d5cc278b60305ab980a6665f46117c5b589c0", ++ "reference": "f60d5cc278b60305ab980a6665f46117c5b589c0", ++ "shasum": "" ++ }, ++ "require": { ++ "ext-mbstring": "*", ++ "php": ">=7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5|^9.4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ssddanbrown\\HtmlDiff\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dan Brown", + "email": "ssddanbrown@googlemail.com", + "role": "Developer" + } + ], + "description": "HTML Content Diff Generator", + "homepage": "https://github.com/ssddanbrown/htmldiff", + "support": { + "issues": "https://github.com/ssddanbrown/HtmlDiff/issues", + "source": "https://github.com/ssddanbrown/HtmlDiff/tree/v1.0.1" + }, + "funding": [ + { + "url": "https://github.com/ssddanbrown", + "type": "github" + } + ], + "time": "2021-01-24T18:51:30+00:00" + }, + { + "name": "swiftmailer/swiftmailer", + "version": "v6.2.7", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", diff --cc resources/views/auth/parts/login-form-openid.blade.php index 000000000,b1ef51d13..ba975ebf4 mode 000000,100644..100644 --- a/resources/views/auth/parts/login-form-openid.blade.php +++ b/resources/views/auth/parts/login-form-openid.blade.php @@@ -1,0 -1,11 +1,11 @@@ +
+ {!! csrf_field() !!} + +
+ +
+ -
++ diff --cc resources/views/settings/roles/form.blade.php index 000000000,b5aa96911..e69de29bb mode 000000,100644..100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php diff --cc resources/views/settings/roles/parts/form.blade.php index 2f94398b5,000000000..3f7f8fd1f mode 100644,000000..100644 --- a/resources/views/settings/roles/parts/form.blade.php +++ b/resources/views/settings/roles/parts/form.blade.php @@@ -1,267 -1,0 +1,267 @@@ +{!! csrf_field() !!} + +
+

{{ $title }}

+ +
+ +
+
+ +
+
+
+ + @include('form.text', ['name' => 'display_name']) +
+
+ + @include('form.text', ['name' => 'description']) +
+
+ @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced') ]) +
+ - @if(config('auth.method') === 'ldap' || config('auth.method') === 'saml2') ++ @if(in_array(config('auth.method'), ['ldap', 'saml2', 'openid'])) +
+ + @include('form.text', ['name' => 'external_auth_id']) +
+ @endif +
+
+ +
+ + {{ trans('common.toggle_all') }} + +
+
+
@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])
+
+
+
@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])
+
@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])
+

{{ trans('settings.roles_system_warning') }}

+
+
+
+ +
+ +

{{ trans('settings.role_asset_desc') }}

+ + @if (isset($role) && $role->system_name === 'admin') +

{{ trans('settings.role_asset_admins') }}

+ @endif + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ trans('common.toggle_all') }} + {{ trans('common.create') }}{{ trans('common.view') }}{{ trans('common.edit') }}{{ trans('common.delete') }}
+
{{ trans('entities.shelves_long') }}
+ {{ trans('common.toggle_all') }} +
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')]) + + @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')]) +
+
{{ trans('entities.books') }}
+ {{ trans('common.toggle_all') }} +
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')]) + + @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')]) +
+
{{ trans('entities.chapters') }}
+ {{ trans('common.toggle_all') }} +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')]) +
+
{{ trans('entities.pages') }}
+ {{ trans('common.toggle_all') }} +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')]) +
+
{{ trans('entities.images') }}
+ {{ trans('common.toggle_all') }} +
@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => '']){{ trans('settings.role_controlled_by_asset') }} + @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')]) +
+
{{ trans('entities.attachments') }}
+ {{ trans('common.toggle_all') }} +
@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => '']){{ trans('settings.role_controlled_by_asset') }} + @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')]) +
+
{{ trans('entities.comments') }}
+ {{ trans('common.toggle_all') }} +
@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => '']){{ trans('settings.role_controlled_by_asset') }} + @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')]) +
+ @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')]) +
+
+
+ +
+ {{ trans('common.cancel') }} + @if (isset($role) && $role->id) + id}") }}" class="button outline">{{ trans('settings.role_delete') }} + @endif + +
+ +
+ +
+

{{ trans('settings.role_users') }}

+ @if(count($role->users ?? []) > 0) +
+ @foreach($role->users as $user) +
+
+ {{ $user->name }} +
+
+ @if(userCan('users-manage') || user()->id == $user->id) + id}") }}"> + @endif + {{ $user->name }} + @if(userCan('users-manage') || user()->id == $user->id) + + @endif +
+
+ @endforeach +
+ @else +

+ {{ trans('settings.role_users_none') }} +

+ @endif +
diff --cc resources/views/users/parts/form.blade.php index 7105e2ff1,f3e8cedff..ef8c611ef --- a/resources/views/users/parts/form.blade.php +++ b/resources/views/users/parts/form.blade.php @@@ -25,7 -25,7 +25,7 @@@ - @if(($authMethod === 'ldap' || $authMethod === 'saml2') && userCan('users-manage')) -@if(($authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'openid') && userCan('users-manage')) ++@if(in_array($authMethod, ['ldap', 'saml2', 'openid']) && userCan('users-manage'))
diff --cc tests/Auth/AuthTest.php index d037b5701,779d5e70f..1ffcc0815 --- a/tests/Auth/AuthTest.php +++ b/tests/Auth/AuthTest.php @@@ -330,39 -400,17 +331,40 @@@ class AuthTest extends TestCas $this->assertTrue(auth()->check()); $this->assertFalse(auth('ldap')->check()); $this->assertFalse(auth('saml2')->check()); + $this->assertFalse(auth('openid')->check()); } + public function test_failed_logins_are_logged_when_message_configured() + { + $log = $this->withTestLogger(); + config()->set(['logging.failed_login.message' => 'Failed login for %u']); + + $this->post('/login', ['email' => 'admin@example.com', 'password' => 'cattreedog']); + $this->assertTrue($log->hasWarningThatContains('Failed login for admin@example.com')); + + $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']); + $this->assertFalse($log->hasWarningThatContains('Failed login for admin@admin.com')); + } + + public function test_logged_in_user_with_unconfirmed_email_is_logged_out() + { + $this->setSettings(['registration-confirmation' => 'true']); + $user = $this->getEditor(); + $user->email_confirmed = false; + $user->save(); + + auth()->login($user); + $this->assertTrue(auth()->check()); + + $this->get('/books')->assertRedirect('/'); + $this->assertFalse(auth()->check()); + } + /** - * Perform a login + * Perform a login. */ - protected function login(string $email, string $password): AuthTest + protected function login(string $email, string $password): TestResponse { - return $this->visit('/login') - ->type($email, '#email') - ->type($password, '#password') - ->press('Log In'); + return $this->post('/login', compact('email', 'password')); } }