]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'auth' of git://github.com/benrubson/BookStack into benrubson-auth
authorDan Brown <redacted>
Tue, 28 Jul 2020 09:46:40 +0000 (10:46 +0100)
committerDan Brown <redacted>
Tue, 28 Jul 2020 09:46:40 +0000 (10:46 +0100)
1  2 
.env.example.complete
app/Actions/ActivityService.php
app/Config/logging.php
app/Http/Controllers/Auth/LoginController.php

diff --combined .env.example.complete
index 472ca051b336388715aab7781c8ec7ab5658978c,5b62b1a2a150a24f98f318b85f2b0a522b11c228..26dfdc54b2efd15749bef423f104a852e103512f
@@@ -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=''
index 9b69cbb1747662240d4c4055f8647f58a17bdb9b,ca09aaef11c49ed1d065d420ff10e6dc50e9b774..0e3ac78611fec3188801e81e75bc6f6231e935de
@@@ -1,9 -1,8 +1,9 @@@
  <?php namespace BookStack\Actions;
  
  use BookStack\Auth\Permissions\PermissionService;
 -use BookStack\Entities\Book;
 +use BookStack\Auth\User;
  use BookStack\Entities\Entity;
 +use Illuminate\Support\Collection;
  
  class ActivityService
  {
@@@ -13,6 -12,8 +13,6 @@@
  
      /**
       * ActivityService constructor.
 -     * @param Activity $activity
 -     * @param PermissionService $permissionService
       */
      public function __construct(Activity $activity, PermissionService $permissionService)
      {
  
      /**
       * Add activity data to database.
 -     * @param \BookStack\Entities\Entity $entity
 -     * @param string $activityKey
 -     * @param int $bookId
       */
 -    public function add(Entity $entity, string $activityKey, int $bookId = null)
 +    public function add(Entity $entity, string $activityKey, ?int $bookId = null)
      {
          $activity = $this->newActivityForUser($activityKey, $bookId);
          $entity->activity()->save($activity);
  
      /**
       * 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
  
      /**
       * 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),
       * 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();
      /**
       * 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);
          }
          
      }
  
      /**
 -     * 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);
      }
  
       * @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)) {
              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 375e84083f9cb647468521cb2a850397684b645e,406b9f2f938892322aa74492d3a4bebc3ee021fc..ba77ba81e87ac5d6b1daf8bdecfc4f38a6207117
@@@ -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', ''),
  ];
index fb2573b5cc2f32abc3f3b70f7b8eeeb0c8d125ab,f5479814a52ac830c2b86cf0a9eb6d42a9fe3543..f031c12cf9873f468292ccbcce8ba71e6d48bc35
@@@ -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('/login')) {
 +            redirect()->setIntendedUrl($previous);
 +        }
 +
          return view('auth.login', [
            'socialDrivers' => $socialDrivers,
            'authMethod' => $authMethod,
              $this->hasTooManyLoginAttempts($request)) {
              $this->fireLockoutEvent($request);
  
+             // Also log some error message
+             Activity::logFailedAccess($request->get($this->username()));
              return $this->sendLockoutResponse($request);
          }
  
          // 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.
       *