Merge branch 'master' into attachment_drag_drop

This commit is contained in:
Dan Brown
2020-09-13 16:33:31 +01:00
47 changed files with 923 additions and 3784 deletions

View File

@ -4,6 +4,7 @@ use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
use BookStack\Entities\Entity;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class ActivityService
{
@ -49,7 +50,7 @@ class ActivityService
protected function newActivityForUser(string $key, ?int $bookId = null): Activity
{
return $this->activity->newInstance()->forceFill([
'key' => strtolower($key),
'key' => strtolower($key),
'user_id' => $this->user->id,
'book_id' => $bookId ?? 0,
]);
@ -64,8 +65,8 @@ class ActivityService
{
$activities = $entity->activity()->get();
$entity->activity()->update([
'extra' => $entity->name,
'entity_id' => 0,
'extra' => $entity->name,
'entity_id' => 0,
'entity_type' => '',
]);
return $activities;
@ -99,7 +100,7 @@ class ActivityService
$query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass())
->where('entity_id', '=', $entity->id);
}
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
@ -159,4 +160,20 @@ class ActivityService
session()->flash('success', $message);
}
}
/**
* Log out a failed login attempt, Providing the given username
* as part of the message if the '%u' string is used.
*/
public function logFailedLogin(string $username)
{
$message = config('logging.failed_login.message');
if (!$message) {
return;
}
$message = str_replace("%u", $username, $message);
$channel = config('logging.failed_login.channel');
Log::channel($channel)->warning($message);
}
}

View File

@ -3,6 +3,8 @@
use BookStack\Auth\Role;
use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ExternalAuthService
{
@ -39,22 +41,14 @@ class ExternalAuthService
/**
* Match an array of group names to BookStack system roles.
* Formats group names to be lower-case and hyphenated.
* @param array $groupNames
* @return \Illuminate\Support\Collection
*/
protected function matchGroupsToSystemsRoles(array $groupNames)
protected function matchGroupsToSystemsRoles(array $groupNames): Collection
{
foreach ($groupNames as $i => $groupName) {
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
}
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
$query->whereIn('name', $groupNames);
foreach ($groupNames as $groupName) {
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
}
})->get();
$roles = Role::query()->get(['id', 'external_auth_id', 'display_name']);
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
return $this->roleMatchesGroupNames($role, $groupNames);
});

View File

@ -71,15 +71,15 @@ class RegistrationService
// Start email confirmation flow if required
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
$newUser->save();
$message = '';
try {
$this->emailConfirmationService->sendConfirmation($newUser);
session()->flash('sent-email-confirmation', true);
} catch (Exception $e) {
$message = trans('auth.email_confirm_send_error');
throw new UserRegistrationException($message, '/register/confirm');
}
throw new UserRegistrationException($message, '/register/confirm');
}
return $newUser;

View File

@ -311,7 +311,6 @@ class Saml2Service extends ExternalAuthService
/**
* Get the user from the database for the specified details.
* @throws SamlException
* @throws UserRegistrationException
*/
protected function getOrRegisterUser(array $userDetails): ?User

View File

@ -3,25 +3,26 @@
use BookStack\Auth\Role;
use BookStack\Entities\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class JointPermission extends Model
{
protected $primaryKey = null;
public $timestamps = false;
/**
* Get the role that this points to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function role()
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* Get the entity this points to.
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
public function entity()
public function entity(): MorphOne
{
return $this->morphOne(Entity::class, 'entity');
}

View File

@ -1,8 +1,9 @@
<?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
class PermissionsRepo
@ -16,11 +17,8 @@ class PermissionsRepo
/**
* PermissionsRepo constructor.
* @param RolePermission $permission
* @param Role $role
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
*/
public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
{
$this->permission = $permission;
$this->role = $role;
@ -29,46 +27,34 @@ class PermissionsRepo
/**
* Get all the user roles from the system.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAllRoles()
public function getAllRoles(): Collection
{
return $this->role->all();
}
/**
* Get all the roles except for the provided one.
* @param Role $role
* @return mixed
*/
public function getAllRolesExcept(Role $role)
public function getAllRolesExcept(Role $role): Collection
{
return $this->role->where('id', '!=', $role->id)->get();
}
/**
* Get a role via its ID.
* @param $id
* @return mixed
*/
public function getRoleById($id)
public function getRoleById($id): Role
{
return $this->role->findOrFail($id);
return $this->role->newQuery()->findOrFail($id);
}
/**
* Save a new role into the system.
* @param array $roleData
* @return Role
*/
public function saveNewRole($roleData)
public function saveNewRole(array $roleData): Role
{
$role = $this->role->newInstance($roleData);
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
$role->name .= strtolower(Str::random(2));
}
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
@ -80,13 +66,11 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensure Admin role always have core permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
*/
public function updateRole($roleId, $roleData)
public function updateRole($roleId, array $roleData)
{
$role = $this->role->findOrFail($roleId);
/** @var Role $role */
$role = $this->role->newQuery()->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
if ($role->system_name === 'admin') {
@ -108,16 +92,19 @@ class PermissionsRepo
/**
* Assign an list of permission names to an role.
* @param Role $role
* @param array $permissionNameArray
*/
public function assignRolePermissions(Role $role, $permissionNameArray = [])
public function assignRolePermissions(Role $role, array $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
if ($permissionNameArray && count($permissionNameArray) > 0) {
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
if ($permissionNameArray) {
$permissions = $this->permission->newQuery()
->whereIn('name', $permissionNameArray)
->pluck('id')
->toArray();
}
$role->permissions()->sync($permissions);
}
@ -126,13 +113,13 @@ class PermissionsRepo
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
* @param $roleId
* @param $migrateRoleId
* @throws PermissionsException
* @throws Exception
*/
public function deleteRole($roleId, $migrateRoleId)
{
$role = $this->role->findOrFail($roleId);
/** @var Role $role */
$role = $this->role->newQuery()->findOrFail($roleId);
// Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
@ -142,9 +129,9 @@ class PermissionsRepo
}
if ($migrateRoleId) {
$newRole = $this->role->find($migrateRoleId);
$newRole = $this->role->newQuery()->find($migrateRoleId);
if ($newRole) {
$users = $role->users->pluck('id')->toArray();
$users = $role->users()->pluck('id')->toArray();
$newRole->users()->sync($users);
}
}

View File

@ -3,6 +3,9 @@
use BookStack\Auth\Role;
use BookStack\Model;
/**
* @property int $id
*/
class RolePermission extends Model
{
/**

View File

@ -3,13 +3,16 @@
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class Role
* @property int $id
* @property string $display_name
* @property string $description
* @property string $external_auth_id
* @package BookStack\Auth
* @property string $system_name
*/
class Role extends Model
{
@ -26,9 +29,8 @@ class Role extends Model
/**
* Get all related JointPermissions.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function jointPermissions()
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class);
}
@ -43,10 +45,8 @@ class Role extends Model
/**
* Check if this role has a permission.
* @param $permissionName
* @return bool
*/
public function hasPermission($permissionName)
public function hasPermission(string $permissionName): bool
{
$permissions = $this->getRelationValue('permissions');
foreach ($permissions as $permission) {
@ -59,7 +59,6 @@ class Role extends Model
/**
* Add a permission to this role.
* @param RolePermission $permission
*/
public function attachPermission(RolePermission $permission)
{
@ -68,7 +67,6 @@ class Role extends Model
/**
* Detach a single permission from this role.
* @param RolePermission $permission
*/
public function detachPermission(RolePermission $permission)
{
@ -76,39 +74,33 @@ class Role extends Model
}
/**
* Get the role object for the specified role.
* @param $roleName
* @return Role
* Get the role of the specified display name.
*/
public static function getRole($roleName)
public static function getRole(string $displayName): ?Role
{
return static::query()->where('name', '=', $roleName)->first();
return static::query()->where('display_name', '=', $displayName)->first();
}
/**
* Get the role object for the specified system role.
* @param $roleName
* @return Role
*/
public static function getSystemRole($roleName)
public static function getSystemRole(string $systemName): ?Role
{
return static::query()->where('system_name', '=', $roleName)->first();
return static::query()->where('system_name', '=', $systemName)->first();
}
/**
* Get all visible roles
* @return mixed
*/
public static function visible()
public static function visible(): Collection
{
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
}
/**
* Get the roles that can be restricted.
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public static function restrictable()
public static function restrictable(): Collection
{
return static::query()->where('system_name', '!=', 'admin')->get();
}

View File

@ -101,12 +101,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasRole($role)
public function hasRole($roleId): bool
{
return $this->roles->pluck('name')->contains($role);
return $this->roles->pluck('id')->contains($roleId);
}
/**
@ -163,7 +161,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Attach a role to this user.
* @param Role $role
*/
public function attachRole(Role $role)
{

View File

@ -238,7 +238,7 @@ class UserRepo
*/
public function getAllRoles()
{
return $this->role->newQuery()->orderBy('name', 'asc')->get();
return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
}
/**

View File

@ -1,5 +1,7 @@
<?php
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
@ -73,6 +75,19 @@ return [
'level' => 'debug',
],
// Custom errorlog implementation that logs out a plain,
// non-formatted message intended for the webserver log.
'errorlog_plain_webserver' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => ErrorLogHandler::class,
'handler_with' => [4],
'formatter' => LineFormatter::class,
'formatter_with' => [
'format' => "%message%",
],
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
@ -86,4 +101,12 @@ return [
],
],
// Failed Login Message
// Allows a configurable message to be logged when a login request fails.
'failed_login' => [
'message' => env('LOG_FAILED_LOGIN_MESSAGE', null),
'channel' => env('LOG_FAILED_LOGIN_CHANNEL', 'errorlog_plain_webserver'),
],
];

View File

@ -101,7 +101,7 @@ return [
'url' => env('SAML2_IDP_SLO', null),
// URL location of the IdP where the SP will send the SLO Response (ResponseLocation)
// if not set, url for the SLO Request will be used
'responseUrl' => '',
'responseUrl' => null,
// SAML protocol binding to be used when returning the <Response>
// message. Onelogin Toolkit supports for this endpoint the
// HTTP-Redirect binding only

View File

@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers\Auth;
use Activity;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
@ -76,9 +77,13 @@ class LoginController extends Controller
]);
}
// Store the previous location for redirect after login
$previous = url()->previous('');
if (setting('app-public') && $previous && $previous !== url('/login')) {
redirect()->setIntendedUrl($previous);
if ($previous && $previous !== url('/login') && setting('app-public')) {
$isPreviousFromInstance = (strpos($previous, url('/')) === 0);
if ($isPreviousFromInstance) {
redirect()->setIntendedUrl($previous);
}
}
return view('auth.login', [
@ -98,6 +103,7 @@ class LoginController extends Controller
public function login(Request $request)
{
$this->validateLogin($request);
$username = $request->get($this->username());
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
@ -106,6 +112,7 @@ class LoginController extends Controller
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
Activity::logFailedLogin($username);
return $this->sendLockoutResponse($request);
}
@ -114,6 +121,7 @@ class LoginController extends Controller
return $this->sendLoginResponse($request);
}
} catch (LoginAttemptException $exception) {
Activity::logFailedLogin($username);
return $this->sendLoginAttemptExceptionResponse($exception, $request);
}
@ -122,6 +130,7 @@ class LoginController extends Controller
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
Activity::logFailedLogin($username);
return $this->sendFailedLoginResponse($request);
}

View File

@ -8,6 +8,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Validation\ValidationException;
abstract class Controller extends BaseController
{
@ -132,23 +133,6 @@ abstract class Controller extends BaseController
return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
}
/**
* Create the response for when a request fails validation.
* @param \Illuminate\Http\Request $request
* @param array $errors
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildFailedValidationResponse(Request $request, array $errors)
{
if ($request->expectsJson()) {
return response()->json(['validation' => $errors], 422);
}
return redirect()->to($this->getRedirectUrl())
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
/**
* Create a response that forces a download in the browser.
* @param string $content

View File

@ -2,7 +2,9 @@
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class PermissionController extends Controller
{
@ -11,7 +13,6 @@ class PermissionController extends Controller
/**
* PermissionController constructor.
* @param \BookStack\Auth\Permissions\PermissionsRepo $permissionsRepo
*/
public function __construct(PermissionsRepo $permissionsRepo)
{
@ -31,7 +32,6 @@ class PermissionController extends Controller
/**
* Show the form to create a new role
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function createRole()
{
@ -41,15 +41,13 @@ class PermissionController extends Controller
/**
* Store a new role in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function storeRole(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
'display_name' => 'required|min:3|max:180',
'description' => 'max:180'
]);
$this->permissionsRepo->saveNewRole($request->all());
@ -59,11 +57,9 @@ class PermissionController extends Controller
/**
* Show the form for editing a user role.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws PermissionsException
*/
public function editRole($id)
public function editRole(string $id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
@ -75,18 +71,14 @@ class PermissionController extends Controller
/**
* Updates a user role.
* @param Request $request
* @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function updateRole(Request $request, $id)
public function updateRole(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
'display_name' => 'required|min:3|max:180',
'description' => 'max:180'
]);
$this->permissionsRepo->updateRole($id, $request->all());
@ -97,10 +89,8 @@ class PermissionController extends Controller
/**
* Show the view to delete a role.
* Offers the chance to migrate users.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showDeleteRole($id)
public function showDeleteRole(string $id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
@ -113,11 +103,9 @@ class PermissionController extends Controller
/**
* Delete a role from the system,
* Migrate from a previous role if set.
* @param Request $request
* @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws Exception
*/
public function deleteRole(Request $request, $id)
public function deleteRole(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');

View File

@ -66,8 +66,8 @@ class UserController extends Controller
{
$this->checkPermission('users-manage');
$validationRules = [
'name' => 'required',
'email' => 'required|email|unique:users,email'
'name' => 'required',
'email' => 'required|email|unique:users,email'
];
$authMethod = config('auth.method');

View File

@ -44,6 +44,10 @@ class Authenticate
], 401);
}
if (session()->get('sent-email-confirmation') === true) {
return redirect('/register/confirm');
}
return redirect('/register/confirm/awaiting');
}
}