- Added testing to cover.
- Linked logging into Laravel's monolog logging system and made log
channel configurable.
- Updated env var names to be specific to login access.
- Added extra locations as to where failed logins would be captured.
Related to #1881 and #728
# The number of API requests that can be made per minute by a single user.
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=''
+# Enable the logging of failed email+password logins with the given message
+# The defaul log channel below uses the php 'error_log' function which commonly
+# results in messages being output to the webserver error logs.
+# The message can contain a %u parameter which will be replaced with the login
+# user identifier (Username or email).
+LOG_FAILED_LOGIN_MESSAGE=false
+LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
use BookStack\Auth\User;
use BookStack\Entities\Entity;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
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,
]);
{
$activities = $entity->activity()->get();
$entity->activity()->update([
- 'extra' => $entity->name,
- 'entity_id' => 0,
+ 'extra' => $entity->name,
+ 'entity_id' => 0,
'entity_type' => '',
]);
return $activities;
$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')
}
/**
- * Log failed accesses, for further processing by tools like Fail2Ban
- *
- * @param username
- * @return void
- */
- public function logFailedAccess($username)
+ * 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)
{
- $log_msg = config('logging.failed_access_message');
-
- if (!is_string($username) || !is_string($log_msg) || strlen($log_msg)<1)
+ $message = config('logging.failed_login.message');
+ if (!$message) {
return;
+ }
- $log_msg = str_replace("%u", $username, $log_msg);
- error_log($log_msg, 4);
+ $message = str_replace("%u", $username, $message);
+ $channel = config('logging.failed_login.channel');
+ Log::channel($channel)->warning($message);
}
}
<?php
+use Monolog\Formatter\LineFormatter;
+use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
'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,
],
],
- // 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', ''),
+
+ // 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'),
+ ],
];
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
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
- // Also log some error message
- Activity::logFailedAccess($request->get($this->username()));
-
+ Activity::logFailedLogin($username);
return $this->sendLockoutResponse($request);
}
return $this->sendLoginResponse($request);
}
} catch (LoginAttemptException $exception) {
+ Activity::logFailedLogin($username);
return $this->sendLoginAttemptExceptionResponse($exception, $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()));
-
+ Activity::logFailedLogin($username);
return $this->sendFailedLoginResponse($request);
}
<server name="DEBUGBAR_ENABLED" value="false"/>
<server name="SAML2_ENABLED" value="false"/>
<server name="API_REQUESTS_PER_MIN" value="180"/>
+ <server name="LOG_FAILED_LOGIN_MESSAGE" value=""/>
+ <server name="LOG_FAILED_LOGIN_CHANNEL" value="testing"/>
</php>
</phpunit>
$this->assertFalse(auth('saml2')->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]'));
+ }
+
/**
* Perform a login
*/
$this->see('A user with the email
[email protected] already exists but with different credentials');
}
+
+ public function test_failed_logins_are_logged_when_message_configured()
+ {
+ $log = $this->withTestLogger();
+ config()->set(['logging.failed_login.message' => 'Failed login for %u']);
+
+ $this->commonLdapMocks(1, 1, 1, 1, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
+ ->andReturn(['count' => 0]);
+
+ $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
+ $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));
+ }
}
<?php namespace Tests\Unit;
+use Illuminate\Support\Facades\Log;
use Tests\TestCase;
/**
$this->checkEnvConfigResult('APP_URL', $oldDefault, 'app.url', '');
}
+ public function test_errorlog_plain_webserver_channel()
+ {
+ // We can't full test this due to it being targeted for the SAPI logging handler
+ // so we just overwrite that component so we can capture the error log output.
+ config()->set([
+ 'logging.channels.errorlog_plain_webserver.handler_with' => [0],
+ ]);
+
+ $temp = tempnam(sys_get_temp_dir(), 'bs-test');
+ $original = ini_set( 'error_log', $temp);
+
+ Log::channel('errorlog_plain_webserver')->info('Aww, look, a cute puppy');
+
+ ini_set( 'error_log', $original);
+
+ $output = file_get_contents($temp);
+ $this->assertStringContainsString('Aww, look, a cute puppy', $output);
+ $this->assertStringNotContainsString('INFO', $output);
+ $this->assertStringNotContainsString('info', $output);
+ $this->assertStringNotContainsString('testing', $output);
+ }
+
/**
* Set an environment variable of the given name and value
* then check the given config key to see if it matches the given result.