use BookStack\Auth\Role;
use BookStack\Auth\User;
+use Illuminate\Support\Collection;
+ use Illuminate\Database\Eloquent\Builder;
+ use Illuminate\Support\Str;
class ExternalAuthService
{
- $user = $this->user->newQuery()
+ 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
+ {
- 'name' => $userDetails['name'],
- 'email' => $userDetails['email'],
- 'password' => Str::random(32),
++ $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),
+ '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.
--- /dev/null
- $guards = ['standard', 'ldap', 'saml2'];
+<?php
+
+namespace BookStack\Auth\Access;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\Access\Mfa\MfaSession;
+use BookStack\Auth\User;
+use BookStack\Exceptions\StoppedAuthenticationException;
+use BookStack\Facades\Activity;
+use BookStack\Facades\Theme;
+use BookStack\Theming\ThemeEvents;
+use Exception;
+
+class LoginService
+{
+ protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
+
+ protected $mfaSession;
+ protected $emailConfirmationService;
+
+ public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
+ {
+ $this->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', '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;
+ }
+}
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;
/**
* 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;
}
/**
'provider' => 'external',
],
'saml2' => [
- 'driver' => 'saml2-session',
+ 'driver' => 'saml2-session',
'provider' => 'external',
],
+ 'openid' => [
+ 'driver' => 'openid-session',
+ 'provider' => 'external',
+ ],
'api' => [
'driver' => 'api-token',
],
*/
protected $except = [
'saml2/*',
- 'openid/*'
++ 'openid/*',
];
}
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
"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": [
"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",
+ "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",
"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",
+ "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",
},
"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",
++ "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",
+ }
+ ],
+ "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",
+ "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",
--- /dev/null
-</form>
+ <form action="{{ url('/openid/login') }}" method="POST" id="login-form" class="mt-l">
+ {!! csrf_field() !!}
+
+ <div>
+ <button id="saml-login" class="button outline block svg">
+ @icon('saml2')
+ <span>{{ trans('auth.log_in_with', ['socialDriver' => config('openid.name')]) }}</span>
+ </button>
+ </div>
+
++</form>
--- /dev/null
- @if(config('auth.method') === 'ldap' || config('auth.method') === 'saml2')
+{!! csrf_field() !!}
+
+<div class="card content-wrap">
+ <h1 class="list-heading">{{ $title }}</h1>
+
+ <div class="setting-list">
+
+ <div class="grid half">
+ <div>
+ <label class="setting-list-label">{{ trans('settings.role_details') }}</label>
+ </div>
+ <div>
+ <div class="form-group">
+ <label for="display_name">{{ trans('settings.role_name') }}</label>
+ @include('form.text', ['name' => 'display_name'])
+ </div>
+ <div class="form-group">
+ <label for="description">{{ trans('settings.role_desc') }}</label>
+ @include('form.text', ['name' => 'description'])
+ </div>
+ <div class="form-group">
+ @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced') ])
+ </div>
+
++ @if(in_array(config('auth.method'), ['ldap', 'saml2', 'openid']))
+ <div class="form-group">
+ <label for="name">{{ trans('settings.role_external_auth_id') }}</label>
+ @include('form.text', ['name' => 'external_auth_id'])
+ </div>
+ @endif
+ </div>
+ </div>
+
+ <div permissions-table>
+ <label class="setting-list-label">{{ trans('settings.role_system') }}</label>
+ <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+
+ <div class="toggle-switch-list grid half mt-m">
+ <div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
+ </div>
+ <div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
+ <div>@include('settings.roles.parts.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
+ <p class="text-warn text-small mt-s mb-none">{{ trans('settings.roles_system_warning') }}</p>
+ </div>
+ </div>
+ </div>
+
+ <div>
+ <label class="setting-list-label">{{ trans('settings.role_asset') }}</label>
+ <p>{{ trans('settings.role_asset_desc') }}</p>
+
+ @if (isset($role) && $role->system_name === 'admin')
+ <p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
+ @endif
+
+ <table permissions-table class="table toggle-switch-list compact permissions-table">
+ <tr>
+ <th width="20%">
+ <a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </th>
+ <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.create') }}</th>
+ <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.view') }}</th>
+ <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.edit') }}</th>
+ <th width="20%" permissions-table-toggle-all-in-column>{{ trans('common.delete') }}</th>
+ </tr>
+ <tr>
+ <td>
+ <div>{{ trans('entities.shelves_long') }}</div>
+ <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div>{{ trans('entities.books') }}</div>
+ <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div>{{ trans('entities.chapters') }}</div>
+ <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div>{{ trans('entities.pages') }}</div>
+ <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div>{{ trans('entities.images') }}</div>
+ <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </td>
+ <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
+ <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div>{{ trans('entities.attachments') }}</div>
+ <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </td>
+ <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
+ <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div>{{ trans('entities.comments') }}</div>
+ <a href="#" permissions-table-toggle-all-in-row class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+ </td>
+ <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
+ <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
+ </td>
+ <td>
+ @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
+ <br>
+ @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <div class="form-group text-right">
+ <a href="{{ url("/settings/roles") }}" class="button outline">{{ trans('common.cancel') }}</a>
+ @if (isset($role) && $role->id)
+ <a href="{{ url("/settings/roles/delete/{$role->id}") }}" class="button outline">{{ trans('settings.role_delete') }}</a>
+ @endif
+ <button type="submit" class="button">{{ trans('settings.role_save') }}</button>
+ </div>
+
+</div>
+
+<div class="card content-wrap auto-height">
+ <h2 class="list-heading">{{ trans('settings.role_users') }}</h2>
+ @if(count($role->users ?? []) > 0)
+ <div class="grid third">
+ @foreach($role->users as $user)
+ <div class="user-list-item">
+ <div>
+ <img class="avatar small" src="{{ $user->getAvatar(40) }}" alt="{{ $user->name }}">
+ </div>
+ <div>
+ @if(userCan('users-manage') || user()->id == $user->id)
+ <a href="{{ url("/settings/users/{$user->id}") }}">
+ @endif
+ {{ $user->name }}
+ @if(userCan('users-manage') || user()->id == $user->id)
+ </a>
+ @endif
+ </div>
+ </div>
+ @endforeach
+ </div>
+ @else
+ <p class="text-muted">
+ {{ trans('settings.role_users_none') }}
+ </p>
+ @endif
+</div>
</div>
</div>
- @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'))
<div class="grid half gap-xl v-center">
<div>
<label class="setting-list-label">{{ trans('settings.users_external_auth_id') }}</label>
$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' => '
[email protected]', 'password' => 'cattreedog']);
+ $this->assertTrue($log->hasWarningThatContains('Failed login for
[email protected]'));
+
+ $this->assertFalse($log->hasWarningThatContains('Failed login for
[email protected]'));
+ }
+
+ 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'));
}
}