Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions config/vufind/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,17 @@ skip_numeric = true
; - 'username' logs the currently logged-in username, if there is one.
;reference_id = false

; A comma-separated list of event types to enable logging of audit events to the
; database audit_event table. Disabled by default.
; The following event types are available:
; ils - Actions involving the library catalog (renewal, requests)
; user - User actions such as logins
; custom - Custom events, for local use
; You should run the "php $VUFIND_HOME/public/index.php util expire_audit_events"
; utility periodically to purge old data in the event table unless you want
; the data to accumulate indefinitely.
;log_audit_events = "ils,user,custom"

; This section can be used to specify a "parent configuration" from which
; the current configuration file will inherit. You can chain multiple
; configurations together if you wish.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TABLE `audit_event` (
`id` int NOT NULL AUTO_INCREMENT,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`type` varchar(50) NOT NULL,
`subtype` varchar(50) NOT NULL,
`user_id` int NULL,
`session_id` varchar(128) NULL,
`username` varchar(255) NULL,
`client_ip` varchar(255) NULL,
`server_ip` varchar(255) NULL,
`server_name` varchar(255) NULL,
`message` varchar(255) NULL,
`data` mediumtext NULL,
PRIMARY KEY (`id`),
KEY `audit_event_user_id_idx` (`user_id`),
CONSTRAINT `audit_event_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE audit_event (
id SERIAL,
date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
type varchar(50) NOT NULL,
subtype varchar(50) NOT NULL,
user_id int,
session_id varchar(128),
username varchar(255),
client_ip varchar(255),
server_ip varchar(255),
server_name varchar(255),
message varchar(255),
data text NULL,
PRIMARY KEY (id)
);
CREATE INDEX audit_event_user_id_idx ON audit_event (user_id);

ALTER TABLE audit_event
ADD CONSTRAINT audit_event_ibfk_1 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE SET NULL;
25 changes: 25 additions & 0 deletions module/VuFind/sql/mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -438,3 +438,28 @@ CREATE TABLE `login_token` (
CONSTRAINT `login_token_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Table structure for table `audit_event`
--

/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `audit_event` (
`id` int NOT NULL AUTO_INCREMENT,
`date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`type` varchar(50) NOT NULL,
`subtype` varchar(50) NOT NULL,
`user_id` int NULL,
`session_id` varchar(128) NULL,
`username` varchar(255) NULL,
`client_ip` varchar(255) NULL,
`server_ip` varchar(255) NULL,
`server_name` varchar(255) NULL,
`message` varchar(255) NULL,
`data` mediumtext NULL,
PRIMARY KEY (`id`),
KEY `audit_event_user_id_idx` (`user_id`),
CONSTRAINT `audit_event_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
28 changes: 28 additions & 0 deletions module/VuFind/sql/pgsql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,27 @@ CREATE TABLE login_token (
CREATE INDEX login_token_user_id_idx ON login_token (user_id);
CREATE INDEX login_token_series_idx ON login_token (series);

--
-- Table structure for table `audit_event`
--

CREATE TABLE audit_event (
id SERIAL,
date timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
type varchar(50) NOT NULL,
subtype varchar(50) NOT NULL,
user_id int,
session_id varchar(128),
username varchar(255),
client_ip varchar(255),
server_ip varchar(255),
server_name varchar(255),
message varchar(255),
data text NULL,
PRIMARY KEY (id)
);
CREATE INDEX audit_event_user_id_idx ON audit_event (user_id);

-- --------------------------------------------------------

--
Expand Down Expand Up @@ -480,6 +501,7 @@ ADD CONSTRAINT feedback_ibfk_1 FOREIGN KEY (user_id) REFERENCES "user" (id) ON D
ADD CONSTRAINT feedback_ibfk_2 FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL;


--
-- Constraints for table access_token
--
ALTER TABLE access_token
Expand All @@ -491,4 +513,10 @@ ADD CONSTRAINT access_token_ibfk_1 FOREIGN KEY (user_id) REFERENCES "user" (id)
ALTER TABLE login_token
ADD CONSTRAINT login_token_ibfk_1 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE;

--
-- Constraints for table audit_event
--
ALTER TABLE audit_event
ADD CONSTRAINT audit_event_ibfk_1 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE SET NULL;

-- --------------------------------------------------------
5 changes: 3 additions & 2 deletions module/VuFind/src/VuFind/Auth/ILSAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -363,9 +363,9 @@ public function newCatalogLogin($username, $password)
* @param array $routeParams Route parameters
* @param array $urlParams URL parameters
*
* @return void
* @return ?array Patron information, or null if not found
*/
public function sendEmailLoginLink($email, $route, $routeParams = [], $urlParams = [])
public function sendEmailLoginLink($email, $route, $routeParams = [], $urlParams = []): ?array
{
if (null === $this->emailAuthenticator) {
throw new \Exception('Email authenticator not set');
Expand All @@ -381,6 +381,7 @@ public function sendEmailLoginLink($email, $route, $routeParams = [], $urlParams
$routeParams
);
}
return $userData;
Comment thread
demiankatz marked this conversation as resolved.
Outdated
}

/**
Expand Down
97 changes: 96 additions & 1 deletion module/VuFind/src/VuFind/Auth/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@
use VuFind\Config\Config;
use VuFind\Cookie\CookieManager;
use VuFind\Db\Entity\UserEntityInterface;
use VuFind\Db\Service\AuditEventServiceInterface;
use VuFind\Db\Service\UserServiceInterface;
use VuFind\Db\Type\AuditEventSubtype;
use VuFind\Db\Type\AuditEventType;
use VuFind\Exception\Auth as AuthException;
use VuFind\ILS\Connection;
use VuFind\Validator\CsrfInterface;
Expand Down Expand Up @@ -121,6 +124,7 @@ class Manager implements
* @param LoginTokenManager $loginTokenManager Login Token manager
* @param Connection $ils ILS connection
* @param RendererInterface $viewRenderer View renderer
* @param AuditEventServiceInterface $auditEventService Event database service
*/
public function __construct(
protected Config $config,
Expand All @@ -132,7 +136,8 @@ public function __construct(
protected CsrfInterface $csrf,
protected LoginTokenManager $loginTokenManager,
protected Connection $ils,
protected RendererInterface $viewRenderer
protected RendererInterface $viewRenderer,
protected AuditEventServiceInterface $auditEventService
) {
// Initialize active authentication setting (defaulting to Database
// if no setting passed in):
Expand Down Expand Up @@ -573,6 +578,13 @@ public function logout(string $url, bool $destroy = true): string
*/
public function clearLoginState(bool $destroy = true): void
{
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::Logout,
$this->userSession->getUserFromSession(),
$destroy ? 'logout' : 'session expired',
);

// Reset authentication state
$this->getAuth()->clearLoginState();

Expand Down Expand Up @@ -632,6 +644,8 @@ public function getUserObject(): ?UserEntityInterface
$this->clearLoginState();
}
} elseif ($user = $this->loginTokenManager->tokenLogin($this->sessionManager->getId())) {
$this->auditEventService->addEvent(AuditEventType::User, AuditEventSubtype::TokenLogin, $user);

if ($this->getAuth() instanceof ChoiceAuth) {
$this->getAuth()->setStrategy($user->getAuthMethod());
}
Expand Down Expand Up @@ -732,6 +746,12 @@ public function create(Request $request): UserEntityInterface
$user = $this->getAuth()->create($request);
$this->updateUser($user, $this->getSelectedAuthMethod());
$this->updateSession($user);
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::Create,
$user,
data: $request->getPost()->toArray()
);
return $user;
}

Expand All @@ -747,6 +767,12 @@ public function updatePassword(Request $request): UserEntityInterface
{
$user = $this->getAuth()->updatePassword($request);
$this->updateSession($user);
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::Update,
$user,
data: ['password' => null]
);
return $user;
}

Expand Down Expand Up @@ -789,6 +815,15 @@ public function updateEmail(UserEntityInterface $user, string $email): void
}
$this->userService->persistEntity($user);
$this->updateSession($user);
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubType::Update,
$user,
data: [
'email' => $email,
'pending' => $user->getPendingEmail() ? true : false,
]
);
}

/**
Expand All @@ -805,6 +840,12 @@ public function updateUserVerifyHash(UserEntityInterface $user): void
$time = str_pad(substr((string)time(), 0, 10), 10, '0', STR_PAD_LEFT);
$user->setVerifyHash($hash . $time);
$this->userService->persistEntity($user);
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::Update,
$user,
data: ['hash' => $user->getVerifyHash()]
);
}

/**
Expand Down Expand Up @@ -854,6 +895,16 @@ public function login(Request $request): UserEntityInterface
try {
$user = $this->getAuth()->authenticate($request);
} catch (AuthException $e) {
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::LoginFailure,
message: $e->getMessage(),
data: [
'main_method' => $mainAuthMethod,
'delegate_method' => $delegate,
'request' => $request->getPost()->toArray(),
]
);
// Pass authentication exceptions through unmodified
throw $e;
} catch (\VuFind\Exception\PasswordSecurity $e) {
Expand All @@ -866,6 +917,17 @@ public function login(Request $request): UserEntityInterface
throw new AuthException('authentication_error_technical', 0, $e);
}

$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::Login,
$user,
data: [
'main_method' => $mainAuthMethod,
'delegate_method' => $delegate,
'request' => $request->getPost()->toArray(),
]
);

// Attempt catalog login so that any bad credentials are cleared before further processing
// (avoids e.g. multiple login attempts by account AJAX checks).
if (
Expand All @@ -888,6 +950,19 @@ public function login(Request $request): UserEntityInterface
// prompted again; perhaps their password has changed in the
// system!
$user->setCatUsername(null)->setRawCatPassword(null)->setCatPassEnc(null);
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::ILSLoginFailure,
$user,
data: ['cat_username' => $catUsername]
);
} else {
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::ILSLogin,
$user,
data: ['cat_username' => $catUsername]
);
}
} catch (\Exception $e) {
// Ignore exceptions here so that the login can continue
Expand All @@ -904,6 +979,11 @@ public function login(Request $request): UserEntityInterface
$this->logError((string)$e);
throw new AuthException('authentication_error_technical', 0, $e);
}
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::RememberLogin,
$user
);
}

// Store the user in the session:
Expand Down Expand Up @@ -1026,6 +1106,12 @@ public function connectLibraryCard(Request $request, UserEntityInterface $user):
throw new \Exception('Connecting of library cards is not supported');
}
$auth->connectLibraryCard($request, $user);
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::ConnectCard,
$user,
data: ['request' => $request->getPost()->toArray()]
);
}

/**
Expand All @@ -1043,6 +1129,15 @@ protected function updateUser(UserEntityInterface $user, ?string $authMethod): v
}
$user->setLastLogin(new \DateTime());
$this->userService->persistEntity($user);
$this->auditEventService->addEvent(
AuditEventType::User,
AuditEventSubtype::Update,
$user,
data: [
'auth_method' => $user->getAuthMethod(),
'last_login' => $user->getLastLogin()?->format('Y-m-d H:i:s'),
Comment thread
demiankatz marked this conversation as resolved.
Outdated
]
);
}

/**
Expand Down
9 changes: 6 additions & 3 deletions module/VuFind/src/VuFind/Auth/ManagerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,17 @@ public function __invoke(
}
// Load dependencies:
$config = $container->get(\VuFind\Config\PluginManager::class)->get('config');
$userService = $container->get(\VuFind\Db\Service\PluginManager::class)
->get(\VuFind\Db\Service\UserServiceInterface::class);
$dbServiceManager = $container->get(\VuFind\Db\Service\PluginManager::class);
$userService = $dbServiceManager->get(\VuFind\Db\Service\UserServiceInterface::class);
$sessionManager = $container->get(\Laminas\Session\SessionManager::class);
$pm = $container->get(\VuFind\Auth\PluginManager::class);
$cookies = $container->get(\VuFind\Cookie\CookieManager::class);
$csrf = $container->get(\VuFind\Validator\CsrfInterface::class);
$loginTokenManager = $container->get(\VuFind\Auth\LoginTokenManager::class);
$ils = $container->get(\VuFind\ILS\Connection::class);
$viewRenderer = $container->get('ViewRenderer');
$auditEventService = $dbServiceManager->get(\VuFind\Db\Service\AuditEventServiceInterface::class);

// Build the object and make sure account credentials haven't expired:
$manager = new $requestedName(
$config,
Expand All @@ -90,7 +92,8 @@ public function __invoke(
$csrf,
$loginTokenManager,
$ils,
$viewRenderer
$viewRenderer,
$auditEventService
);
$manager->setIlsAuthenticator($container->get(\VuFind\Auth\ILSAuthenticator::class));
$manager->checkForExpiredCredentials();
Expand Down
Loading