From: Dan Brown Date: Tue, 28 Jul 2020 09:46:40 +0000 (+0100) Subject: Merge branch 'auth' of git://github.com/benrubson/BookStack into benrubson-auth X-Git-Tag: v0.30.0~1^2~4^2~12 X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/2f6ff0734773c4ac009de699a2661971fd585b22?ds=inline;hp=-c Merge branch 'auth' of git://github.com/benrubson/BookStack into benrubson-auth --- 2f6ff0734773c4ac009de699a2661971fd585b22 diff --combined .env.example.complete index 472ca051b,5b62b1a2a..26dfdc54b --- a/.env.example.complete +++ b/.env.example.complete @@@ -200,7 -200,6 +200,7 @@@ LDAP_ID_ATTRIBUTE=ui LDAP_EMAIL_ATTRIBUTE=mail LDAP_DISPLAY_NAME_ATTRIBUTE=cn LDAP_FOLLOW_REFERRALS=true +LDAP_DUMP_USER_DETAILS=false # LDAP group sync configuration # Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/ @@@ -238,10 -237,7 +238,10 @@@ DISABLE_EXTERNAL_SERVICES=fals # Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon AVATAR_URL= -# Enable Draw.io integration +# Enable draw.io integration +# Can simply be true/false to enable/disable the integration. +# Alternatively, It can be URL to the draw.io instance you want to use. +# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1 DRAWIO=true # Default item listing view @@@ -270,4 -266,10 +270,10 @@@ API_DEFAULT_ITEM_COUNT=10 API_MAX_ITEM_COUNT=500 # The number of API requests that can be made per minute by a single user. - API_REQUESTS_PER_MIN=180 + API_REQUESTS_PER_MIN=180 + + # Failed access + # message to log into webserver logs in case of failed access, for further processing by tools like Fail2Ban + # Apache users should use : user "%u" authentication failure for "BookStack" + # Nginx users should use : user "%u" was not found in "BookStack" + FAILED_ACCESS_MESSAGE='' diff --combined app/Actions/ActivityService.php index 9b69cbb17,ca09aaef1..0e3ac7861 --- a/app/Actions/ActivityService.php +++ b/app/Actions/ActivityService.php @@@ -1,9 -1,8 +1,9 @@@ newActivityForUser($activityKey, $bookId); $entity->activity()->save($activity); @@@ -33,8 -37,11 +33,8 @@@ /** * Adds a activity history with a message, without binding to a entity. - * @param string $activityKey - * @param string $message - * @param int $bookId */ - public function addMessage(string $activityKey, string $message, int $bookId = null) + public function addMessage(string $activityKey, string $message, ?int $bookId = null) { $this->newActivityForUser($activityKey, $bookId)->forceFill([ 'extra' => $message @@@ -45,8 -52,11 +45,8 @@@ /** * Get a new activity instance for the current user. - * @param string $key - * @param int|null $bookId - * @return Activity */ - protected function newActivityForUser(string $key, int $bookId = null) + protected function newActivityForUser(string $key, ?int $bookId = null): Activity { return $this->activity->newInstance()->forceFill([ 'key' => strtolower($key), @@@ -59,27 -69,34 +59,27 @@@ * Removes the entity attachment from each of its activities * and instead uses the 'extra' field with the entities name. * Used when an entity is deleted. - * @param \BookStack\Entities\Entity $entity - * @return mixed */ - public function removeEntity(Entity $entity) + public function removeEntity(Entity $entity): Collection { - // TODO - Rewrite to db query. - $activities = $entity->activity; - foreach ($activities as $activity) { - $activity->extra = $entity->name; - $activity->entity_id = 0; - $activity->entity_type = null; - $activity->save(); - } + $activities = $entity->activity()->get(); + $entity->activity()->update([ + 'extra' => $entity->name, + 'entity_id' => 0, + 'entity_type' => '', + ]); return $activities; } /** * Gets the latest activity. - * @param int $count - * @param int $page - * @return array */ - public function latest($count = 20, $page = 0) + public function latest(int $count = 20, int $page = 0): array { $activityList = $this->permissionService ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') ->orderBy('created_at', 'desc') - ->with('user', 'entity') + ->with(['user', 'entity']) ->skip($count * $page) ->take($count) ->get(); @@@ -90,13 -107,17 +90,13 @@@ /** * Gets the latest activity for an entity, Filtering out similar * items to prevent a message activity list. - * @param \BookStack\Entities\Entity $entity - * @param int $count - * @param int $page - * @return array */ - public function entityActivity($entity, $count = 20, $page = 1) + public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array { if ($entity->isA('book')) { - $query = $this->activity->where('book_id', '=', $entity->id); + $query = $this->activity->newQuery()->where('book_id', '=', $entity->id); } else { - $query = $this->activity->where('entity_type', '=', $entity->getMorphClass()) + $query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass()) ->where('entity_id', '=', $entity->id); } @@@ -112,18 -133,18 +112,18 @@@ } /** - * Get latest activity for a user, Filtering out similar - * items. - * @param $user - * @param int $count - * @param int $page - * @return array + * Get latest activity for a user, Filtering out similar items. */ - public function userActivity($user, $count = 20, $page = 0) + public function userActivity(User $user, int $count = 20, int $page = 0): array { $activityList = $this->permissionService ->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type') - ->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get(); + ->orderBy('created_at', 'desc') + ->where('user_id', '=', $user->id) + ->skip($count * $page) + ->take($count) + ->get(); + return $this->filterSimilar($activityList); } @@@ -132,26 -153,29 +132,26 @@@ * @param Activity[] $activities * @return array */ - protected function filterSimilar($activities) + protected function filterSimilar(iterable $activities): array { $newActivity = []; - $previousItem = false; + $previousItem = null; + foreach ($activities as $activityItem) { - if ($previousItem === false) { - $previousItem = $activityItem; - $newActivity[] = $activityItem; - continue; - } - if (!$activityItem->isSimilarTo($previousItem)) { + if (!$previousItem || !$activityItem->isSimilarTo($previousItem)) { $newActivity[] = $activityItem; } + $previousItem = $activityItem; } + return $newActivity; } /** * Flashes a notification message to the session if an appropriate message is available. - * @param $activityKey */ - protected function setNotification($activityKey) + protected function setNotification(string $activityKey) { $notificationTextKey = 'activities.' . $activityKey . '_notification'; if (trans()->has($notificationTextKey)) { @@@ -159,4 -183,21 +159,21 @@@ session()->flash('success', $message); } } + + /** + * Log failed accesses, for further processing by tools like Fail2Ban + * + * @param username + * @return void + */ + public function logFailedAccess($username) + { + $log_msg = config('logging.failed_access_message'); + + if (!is_string($username) || !is_string($log_msg) || strlen($log_msg)<1) + return; + + $log_msg = str_replace("%u", $username, $log_msg); + error_log($log_msg, 4); + } } diff --combined app/Config/logging.php index 375e84083,406b9f2f9..ba77ba81e --- a/app/Config/logging.php +++ b/app/Config/logging.php @@@ -77,13 -77,11 +77,18 @@@ return 'driver' => 'monolog', 'handler' => NullHandler::class, ], + + // Testing channel + // Uses a shared testing instance during tests + // so that logs can be checked against. + 'testing' => [ + 'driver' => 'testing', + ], ], + // Failed Access Message + // Defines the message to log into webserver logs in case of failed access, + // for further processing by tools like Fail2Ban. + 'failed_access_message' => env('FAILED_ACCESS_MESSAGE', ''), + ]; diff --combined app/Http/Controllers/Auth/LoginController.php index fb2573b5c,f5479814a..f031c12cf --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@@ -2,6 -2,7 +2,7 @@@ namespace BookStack\Http\Controllers\Auth; + use Activity; use BookStack\Auth\Access\SocialAuthService; use BookStack\Exceptions\LoginAttemptEmailNeededException; use BookStack\Exceptions\LoginAttemptException; @@@ -76,11 -77,6 +77,11 @@@ class LoginController extends Controlle ]); } + $previous = url()->previous(''); + if (setting('app-public') && $previous && $previous !== url('/http/source.bookstackapp.com/login')) { + redirect()->setIntendedUrl($previous); + } + return view('auth.login', [ 'socialDrivers' => $socialDrivers, 'authMethod' => $authMethod, @@@ -106,6 -102,9 +107,9 @@@ $this->hasTooManyLoginAttempts($request)) { $this->fireLockoutEvent($request); + // Also log some error message + Activity::logFailedAccess($request->get($this->username())); + return $this->sendLockoutResponse($request); } @@@ -122,29 -121,12 +126,32 @@@ // user surpasses their maximum number of attempts they will get locked out. $this->incrementLoginAttempts($request); + // Also log some error message + Activity::logFailedAccess($request->get($this->username())); + return $this->sendFailedLoginResponse($request); } + /** + * The user has been authenticated. + * + * @param \Illuminate\Http\Request $request + * @param mixed $user + * @return mixed + */ + protected function authenticated(Request $request, $user) + { + // Authenticate on all session guards if a likely admin + if ($user->can('users-manage') && $user->can('user-roles-manage')) { + $guards = ['standard', 'ldap', 'saml2']; + foreach ($guards as $guard) { + auth($guard)->login($user); + } + } + + return redirect()->intended($this->redirectPath()); + } + /** * Validate the user login request. *