From d0f7b47e41b8a2cd07817bfd5b66ff82d58cf0fa Mon Sep 17 00:00:00 2001 From: Antonio Tessarolo Date: Tue, 16 Feb 2021 17:09:36 +0100 Subject: [PATCH 1/5] Add Italian translation This commit adds support for Italian language. --- approot/messages_it.xml | 225 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 approot/messages_it.xml diff --git a/approot/messages_it.xml b/approot/messages_it.xml new file mode 100644 index 00000000..e8972d0a --- /dev/null +++ b/approot/messages_it.xml @@ -0,0 +1,225 @@ + + + + +Aggiungi +Amministrazione +Applica +Annulla +Annulla +Crea +Caricamento... +Login + Logout +Non è una directory +Password +Errata combinazione di Login / Password +Superati i tentativi di accesso, riprova più tardi +Conferma la password +Il campo password non deve essere vuoto +Nuova password +Vecchia password +La password è troppo debole +Le password non corrispondono +Un'altra sessione è stata aperta. Vuoi riaprire questa? +Salva +Utente + + +Artista non trovato +C'è stato un errore! +Ritorna alla home +Album non trovato +Non sei autorizzato ad eseguire questa operazione +Utente non trovato + + +Giornaliera +Raccolta musicale +Ogni ora +Scansiona ora! +Mensile + Raccolta musicale +Mai +Cartella dei file multimediali +Modalità di raccomandazione brani consigliati +Basata sui tag +Basata sull'analisi acustica +Scansione completata: {1} file totali, {2} aggiunti, {3} aggiornati, {4} eliminati, {5} duplicati, {6} errati +Scansione avviata! +Impostazioni scansione +Impostazioni salvate! +Tags +Frequenza di aggiornamento +Orario di aggiornamento +Settimanale + +Non sono stato in grado di determinare la durata della traccia +Non in grado di analizzare il file +Non in grado di leggere il file +{1} file duplicati: +{1} errori: +Forza una nuova scansione completa +Scarica un resoconto +Ultima scansione +Non disponibile +Scansionati {1} files in {2} su {3} ({4} errori, {5} duplicati) +Nessuna traccia audio +Hash doppio +MBID doppio +Scansiona ora +Scanner +Stato +Non pianificato +Pianificato il {1} +Scansione: passo {1}/{2} +Controllo file... {1}% +File trovati: {1} files +Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)... +Ricarica motore di tracce simili: {1}%... +Scansione files: {1}/{2} files ({3}%)... + + +Nuovo utente +Amministratore +Elimina +Elimina l'utente +Vuoi eliminare l'utente? +Demo +Modifica +Utenti + Utenti + + +Modalità di autenticazione +Interna +PAM +Account demo +L'account demo è già esistente! +La password dell'account demo deve essere il nome utente! +Ultimo accesso +Utente già esistente! +Crea utente +Nuovo utente creato! +Modifica l'utente {1} +Utente aggiornato! + + +L'account amministratore è stato creato. Per favore ricarica la pagina per continuare! +Crea un account amministratore + + +Ricordami +Benvenuto! + + +Aggiungi filtro +Tutti +Artisti +Download +Filtro aggiunto +Filtri +Collegamenti +Più ascoltati +Artista MusicBrainz +Album MusicBrainz +Aggiungi alla coda di riproduzione +Aggiungi alla coda di riproduzione mischiando +Casuali +Aggiunti di recente +Riprodotti di recente +Album +Aggiungi ai preferiti +Preferiti +Tracce +Tipo +Rimuovi dai preferiti +Valore +Vari artisti + + +Artisti simili + + +Tutti gli artisti +Artisti tracce +Compositori +Scrittori +Mixers +Produttori +Artisti album +Remixers + + +Album simili +Copyright +Disco {1} + + +Ricerca... +Risultati di ricerca + + +Transcodifica attiva + + +Cancella +{1} tracce + + Aggiunta {1} traccia + Aggiunte {1} tracce + +Coda di riproduzione +Coda di riproduzione piena! +Modalità radio +Ripeti +Mischia + + +Storico riproduzione + + + +Aspetto +Audio +Queste impostazioni sono salvate localmente su questo dispositivo! +Cambia password +Tema scuro +Non posso salvare le impostazioni utilizzando l'account demo! + Impostazioni +Password errata +Devi inserire la password attuale +Modalità ReplayGain +No ReplayGain +Automatica +Traccia +Album +Preamplificazione ReplayGain +Preamplificazione ReplayGain (se non è disponibile nessuna informazione) +Modalità di elencazione artisti +Tutti gli artisti +Artisti album +Artisti tracce +Subsonic API +Transcodifica +Bitrate transcodifica +Abilita transcodifica +Formato di transcodifica +Matroska/Opus +MP3 +Ogg/Opus +Ogg/Vorbis +WebM/Vorbis +Abilita transcodifica +Sempre +Quando il formato non è supportato +Mai +Nuove impostazioni salvate! + + +Si +No +Questo campo non può essere vuoto +Il numero deve essere compreso tra {1} e {2} + From b94fe3e852d340d56e0bbad81063e0783f9d0b29 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 3 Mar 2021 17:42:39 +0100 Subject: [PATCH 2/5] Construct at least a 2x2 network to classify tracks. fixes #116 --- .../recommendation/impl/features/FeaturesClassifier.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp index b28f2160..f0ccf38d 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifier.cpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifier.cpp @@ -194,7 +194,12 @@ FeaturesClassifier::loadFromTraining(Database::Session& session, const TrainSett for (auto& sample : samples) dataNormalizer.normalizeData(sample); - const SOM::Coordinate size {static_cast(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))}; + SOM::Coordinate size {static_cast(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))}; + if (size < 2) + { + LMS_LOG(RECOMMENDATION, WARNING) << "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors"; + size = 2; + } LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network"; SOM::Network network {size, size, nbDimensions}; From cc28d893f5ee705eb2ec29e2cc09de60140c3805 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 4 Mar 2021 19:32:09 +0100 Subject: [PATCH 3/5] Added authentication backends: internal, pam and http-headers. fixes #119 --- CMakeLists.txt | 1 + INSTALL.md | 12 +- README.md | 5 +- approot/admin-database.xml | 8 +- approot/admin-initwizard.xml | 9 - approot/admin-user.xml | 11 +- approot/admin-users.xml | 2 + approot/messages.xml | 5 +- approot/messages_fr.xml | 5 +- approot/messages_it.xml | 4 - approot/settings.xml | 5 +- conf/lms.conf | 11 +- src/libs/auth/CMakeLists.txt | 9 +- src/libs/auth/impl/AuthServiceBase.cpp | 55 +++++ src/libs/auth/impl/AuthServiceBase.hpp | 38 ++++ src/libs/auth/impl/AuthTokenService.cpp | 142 ++++++------ src/libs/auth/impl/AuthTokenService.hpp | 24 +-- src/libs/auth/impl/EnvService.cpp | 35 +++ src/libs/auth/impl/PasswordService.cpp | 156 -------------- src/libs/auth/impl/PasswordService.hpp | 57 ----- src/libs/auth/impl/PasswordServiceBase.cpp | 99 +++++++++ src/libs/auth/impl/PasswordServiceBase.hpp | 64 ++++++ .../http-headers/HttpHeadersEnvService.cpp | 52 +++++ .../http-headers/HttpHeadersEnvService.hpp | 39 ++++ .../impl/internal/InternalPasswordService.cpp | 122 +++++++++++ .../impl/internal/InternalPasswordService.hpp | 55 +++++ src/libs/auth/impl/pam/PAM.cpp | 183 ---------------- src/libs/auth/impl/pam/PAMPasswordService.cpp | 204 ++++++++++++++++++ src/libs/auth/impl/pam/PAMPasswordService.hpp | 46 ++++ .../auth/include/auth/IAuthTokenService.hpp | 24 ++- src/libs/auth/include/auth/IEnvService.hpp | 62 ++++++ .../auth/include/auth/IPasswordService.hpp | 49 +++-- src/libs/auth/include/auth/Types.hpp | 37 ++++ src/libs/cover/CMakeLists.txt | 3 +- src/libs/database/impl/Session.cpp | 71 +++--- src/libs/database/impl/StringViewTraits.hpp | 33 +++ src/libs/database/impl/User.cpp | 15 +- src/libs/database/include/database/Db.hpp | 7 +- .../database/include/database/Session.hpp | 88 ++++---- src/libs/database/include/database/User.hpp | 21 +- src/libs/scanner/impl/AcousticBrainzUtils.cpp | 4 +- src/libs/subsonic/impl/SubsonicResource.cpp | 142 ++++++++---- src/libs/utils/CMakeLists.txt | 1 + src/libs/utils/impl/Config.cpp | 40 ++-- src/libs/utils/impl/Config.hpp | 10 +- src/libs/utils/impl/RecursiveSharedMutex.cpp | 112 ++++++++++ src/libs/utils/include/utils/IConfig.hpp | 13 +- .../include/utils/RecursiveSharedMutex.hpp | 45 ++++ src/libs/utils/include/utils/Service.hpp | 8 +- src/lms/CMakeLists.txt | 6 +- src/lms/main.cpp | 40 +++- src/lms/ui/Auth.cpp | 26 ++- src/lms/ui/Auth.hpp | 3 - src/lms/ui/LmsApplication.cpp | 150 +++++++------ src/lms/ui/LmsApplication.hpp | 28 ++- src/lms/ui/LmsApplicationException.hpp | 6 + src/lms/ui/SettingsView.cpp | 131 +++++------ src/lms/ui/admin/DatabaseSettingsView.cpp | 5 +- src/lms/ui/admin/InitWizardView.cpp | 112 ++-------- src/lms/ui/admin/UserView.cpp | 161 +++++--------- src/lms/ui/admin/UsersView.cpp | 15 +- src/lms/ui/common/DirectoryValidator.cpp | 56 +++++ .../ui/common/DirectoryValidator.hpp} | 14 +- ...{Validators.hpp => LoginNameValidator.cpp} | 29 ++- ...thModeModel.hpp => LoginNameValidator.hpp} | 11 +- src/lms/ui/common/MandatoryValidator.cpp | 31 +++ src/lms/ui/common/MandatoryValidator.hpp | 28 +++ src/lms/ui/common/PasswordValidator.cpp | 96 +++++++++ ...uthModeModel.cpp => PasswordValidator.hpp} | 27 +-- src/lms/ui/common/Validators.cpp | 70 ------ src/test/CMakeLists.txt | 1 + src/test/utils/CMakeLists.txt | 12 ++ src/test/utils/UtilsTest.cpp | 121 +++++++++++ 73 files changed, 2188 insertions(+), 1234 deletions(-) create mode 100644 src/libs/auth/impl/AuthServiceBase.cpp create mode 100644 src/libs/auth/impl/AuthServiceBase.hpp create mode 100644 src/libs/auth/impl/EnvService.cpp delete mode 100644 src/libs/auth/impl/PasswordService.cpp delete mode 100644 src/libs/auth/impl/PasswordService.hpp create mode 100644 src/libs/auth/impl/PasswordServiceBase.cpp create mode 100644 src/libs/auth/impl/PasswordServiceBase.hpp create mode 100644 src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp create mode 100644 src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp create mode 100644 src/libs/auth/impl/internal/InternalPasswordService.cpp create mode 100644 src/libs/auth/impl/internal/InternalPasswordService.hpp delete mode 100644 src/libs/auth/impl/pam/PAM.cpp create mode 100644 src/libs/auth/impl/pam/PAMPasswordService.cpp create mode 100644 src/libs/auth/impl/pam/PAMPasswordService.hpp create mode 100644 src/libs/auth/include/auth/IEnvService.hpp create mode 100644 src/libs/auth/include/auth/Types.hpp create mode 100644 src/libs/database/impl/StringViewTraits.hpp create mode 100644 src/libs/utils/impl/RecursiveSharedMutex.cpp create mode 100644 src/libs/utils/include/utils/RecursiveSharedMutex.hpp create mode 100644 src/lms/ui/common/DirectoryValidator.cpp rename src/{libs/auth/impl/pam/PAM.hpp => lms/ui/common/DirectoryValidator.hpp} (76%) rename src/lms/ui/common/{Validators.hpp => LoginNameValidator.cpp} (67%) rename src/lms/ui/common/{AuthModeModel.hpp => LoginNameValidator.hpp} (75%) create mode 100644 src/lms/ui/common/MandatoryValidator.cpp create mode 100644 src/lms/ui/common/MandatoryValidator.hpp create mode 100644 src/lms/ui/common/PasswordValidator.cpp rename src/lms/ui/common/{AuthModeModel.cpp => PasswordValidator.hpp} (51%) delete mode 100644 src/lms/ui/common/Validators.cpp create mode 100644 src/test/utils/CMakeLists.txt create mode 100644 src/test/utils/UtilsTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a807b62..23dbb83e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED True) include(CTest) +find_package(Threads REQUIRED) find_package(Filesystem REQUIRED) find_package(FFMPEGAV REQUIRED) find_package(Boost REQUIRED COMPONENTS system program_options) diff --git a/INSTALL.md b/INSTALL.md index 3e408ce7..5d923e06 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -8,6 +8,7 @@ + [Upgrade](#upgrade) - [Deployment](#deployment) * [Configuration](#configuration) + * [Authentication backend](#authentication-backend) * [Deploy on non root path](#deploy-on-non-root-path) * [Reverse proxy settings](#reverse-proxy-settings) - [Run](#run) @@ -125,7 +126,16 @@ _LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is All other settings are set using the web interface (user management, scan settings, transcode settings, ...). -If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default.conf](conf/lms.conf) file) +If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default configuration file](conf/lms.conf)) + +## Authentication backend + +You can define which authentication backend to be used thanks to the `authentication-backend` option: +* `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users. +* `PAM`: the authentication request is forwarded to PAM (see the [default configuration file](conf/pam/lms)). +* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-user-field` option. + +__Note__: the first created user is the admin user ## Deploy on non root path If you want to deploy on non root path (e.g. https://mydomain.com/newroot/), you have to set the `deploy-path` option accordingly in `lms.conf`. diff --git a/README.md b/README.md index 32ffdae2..516f3e5f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ A [demo instance](http://lms.demo.poupon.io) is available. Note the administrati * Persistent play queue across sessions * _Systemd_ integration * User management - * with optional _PAM_ authentication backend * Subsonic API, with the following additional features: * Playlists * Bookmarks @@ -30,7 +29,7 @@ _LMS_ provides several ways to help you find the music you like: * Radio mode, based on what is in the current playqueue * Searches in album, artist and track names (including sort names) * Starred Albums/Artists/Tracks -* Custom tags support to help you filter your music: _mood_, _albummood_, _albumgenre_, _albumgrouping_, ... +* Various tags to help you filter your music: _mood_, _albummood_, _albumgenre_, _albumgrouping_, ... * Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like: * Recently added _Electronic_ artists * Random _Metal_ and _Aggressive_ albums @@ -54,7 +53,7 @@ Since _LMS_ uses metadata tags to organize music, a compatibility mode is used t The Subsonic API is enabled by default. -__Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the __token authentication__ method defined from version 1.13.0. +__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method defined from version 1.13.0. ## Keyboard shortcuts * Play/pause: Space diff --git a/approot/admin-database.xml b/approot/admin-database.xml index 91e9347b..8b6a1173 100644 --- a/approot/admin-database.xml +++ b/approot/admin-database.xml @@ -58,9 +58,11 @@ -
-
- ${apply-btn class="btn-primary"} ${discard-btn} +
+
+
+ ${apply-btn class="btn-primary"} ${discard-btn} +
diff --git a/approot/admin-initwizard.xml b/approot/admin-initwizard.xml index dce9da57..e86ba76d 100644 --- a/approot/admin-initwizard.xml +++ b/approot/admin-initwizard.xml @@ -20,15 +20,6 @@ ${admin-login-info class="help-block"}
-
- -
- ${auth-mode} - ${auth-mode-info class="help-block"} -
-
${} -
- -
- ${auth-mode} - ${auth-mode-info class="help-block"} -
-
+ ${}
+ ${
} ${}
diff --git a/approot/admin-users.xml b/approot/admin-users.xml index 8407e944..0e8da5ce 100644 --- a/approot/admin-users.xml +++ b/approot/admin-users.xml @@ -6,7 +6,9 @@

${tr:Lms.Admin.Users.users}

${users} + ${} ${add-btn class="btn-primary Lms-admin-users-add-btn"} + ${} diff --git a/approot/messages.xml b/approot/messages.xml index 43389b1e..28dff4ae 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -17,7 +17,6 @@ Bad login / password combination Login throttled, please try again later Confirm password -Password must not be empty New password Old password Password too weak @@ -29,6 +28,7 @@ Artist not found Error occured! +Deployment error Go home Release not found You are not allowed to perform this operation @@ -92,9 +92,6 @@ Users -Authentication mode -Internal -PAM Demo account Demo account already exists! Demo password must be the login name! diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index d4d1d5fe..641029b3 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -17,7 +17,6 @@ Mauvaise combinaison login / mot de passe Trop de tentatives de connexion, veuillez réessayer plus tard Confirmation du mot de passe -Le mot de passe ne doit pas être vide Nouveau mot de passe Ancien mot de passe Mot de passe trop faible @@ -29,6 +28,7 @@ Cet artiste n'existe pas Une erreur est survenue! +Erreur de déploiement Retour à l'accueil Cet album n'existe pas Vous n'avez pas les droits pour effectuer cette opération @@ -92,9 +92,6 @@ Utilisateurs -Mode d'authentification -Interne -PAM Compte de démonstration Le compte de démonstration existe déjà ! Le password doit être égal au login ! diff --git a/approot/messages_it.xml b/approot/messages_it.xml index e8972d0a..c50c3daa 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -17,7 +17,6 @@ Errata combinazione di Login / Password Superati i tentativi di accesso, riprova più tardi Conferma la password -Il campo password non deve essere vuoto Nuova password Vecchia password La password è troppo debole @@ -92,9 +91,6 @@ Utenti -Modalità di autenticazione -Interna -PAM Account demo L'account demo è già esistente! La password dell'account demo deve essere il nome utente! diff --git a/approot/settings.xml b/approot/settings.xml index 6b52ee30..c5df89f0 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -131,6 +131,7 @@
${} + ${} ${tr:Lms.Settings.change-password}
${} @@ -162,7 +163,9 @@ ${password-confirm-info class="help-block"}
- + + ${
} +
${apply-btn class="btn-primary"} ${discard-btn} diff --git a/conf/lms.conf b/conf/lms.conf index 471bdaa2..fc38a121 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -37,15 +37,20 @@ http-server-thread-count = 0; # Acoustic brainz's root API acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/"; +# Authentication +# Available backends: "internal", "PAM", "http-headers" +authentication-backend = "internal"; +http-headers-user-field = "X-Forwarded-User"; + +# Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used) +login-throttler-max-entries = 10000; + # API api-subsonic = true; # Turn on this option to allow the demo account creation/use demo = false; -# Max entries in the login throttler (1 entry per client) -login-throttler-max-entries = 10000; - # Max external cover file size in MBytes cover-max-file-size = 10; diff --git a/src/libs/auth/CMakeLists.txt b/src/libs/auth/CMakeLists.txt index bceb83c3..07400a6b 100644 --- a/src/libs/auth/CMakeLists.txt +++ b/src/libs/auth/CMakeLists.txt @@ -1,8 +1,12 @@ add_library(lmsauth SHARED impl/AuthTokenService.cpp - impl/PasswordService.cpp + impl/AuthServiceBase.cpp + impl/EnvService.cpp impl/LoginThrottler.cpp + impl/PasswordServiceBase.cpp + impl/http-headers/HttpHeadersEnvService.cpp + impl/internal/InternalPasswordService.cpp ) target_include_directories(lmsauth INTERFACE @@ -11,6 +15,7 @@ target_include_directories(lmsauth INTERFACE target_include_directories(lmsauth PRIVATE include + impl ) target_link_libraries(lmsauth PRIVATE @@ -26,7 +31,7 @@ target_link_libraries(lmsauth PUBLIC if (USE_PAM) target_compile_options(lmsauth PRIVATE "-DLMS_SUPPORT_PAM") - target_sources(lmsauth PRIVATE impl/pam/PAM.cpp) + target_sources(lmsauth PRIVATE impl/pam/PAMPasswordService.cpp) target_include_directories(lmsauth PRIVATE ${PAM_INCLUDE_DIR}) target_link_libraries(lmsauth PRIVATE ${PAM_LIBRARIES}) endif (USE_PAM) diff --git a/src/libs/auth/impl/AuthServiceBase.cpp b/src/libs/auth/impl/AuthServiceBase.cpp new file mode 100644 index 00000000..a46280f2 --- /dev/null +++ b/src/libs/auth/impl/AuthServiceBase.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "AuthServiceBase.hpp" + +#include "database/Session.hpp" +#include "database/User.hpp" +#include "utils/Logger.hpp" + +namespace Auth +{ + Database::IdType + AuthServiceBase::getOrCreateUser(Database::Session& session, std::string_view loginName) + { + auto transaction {session.createUniqueTransaction()}; + + Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; + if (!user) + { + const Database::User::Type type {Database::User::getCount(session) == 0 ? Database::User::Type::ADMIN : Database::User::Type::REGULAR}; + + LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == Database::User::Type::ADMIN); + + user = Database::User::create(session, loginName); + user.modify()->setType(type); + } + + return user.id(); + } + + void + AuthServiceBase::onUserAuthenticated(Database::Session& session, Database::IdType userId) + { + auto transaction {session.createUniqueTransaction()}; + Database::User::pointer user {Database::User::getById(session, userId)}; + if (user) + user.modify()->setLastLogin(Wt::WDateTime::currentDateTime()); + } +} diff --git a/src/libs/auth/impl/AuthServiceBase.hpp b/src/libs/auth/impl/AuthServiceBase.hpp new file mode 100644 index 00000000..8a7672e1 --- /dev/null +++ b/src/libs/auth/impl/AuthServiceBase.hpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include "database/Types.hpp" + +namespace Database +{ + class Session; +} + +namespace Auth +{ + class AuthServiceBase + { + protected: + Database::IdType getOrCreateUser(Database::Session& session, std::string_view loginName); + void onUserAuthenticated(Database::Session& session, Database::IdType userId); + }; +} diff --git a/src/libs/auth/impl/AuthTokenService.cpp b/src/libs/auth/impl/AuthTokenService.cpp index ff21dbeb..b22a5ac1 100644 --- a/src/libs/auth/impl/AuthTokenService.cpp +++ b/src/libs/auth/impl/AuthTokenService.cpp @@ -23,103 +23,115 @@ #include #include +#include "auth/Types.hpp" #include "database/Session.hpp" #include "database/User.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" -namespace Auth { - -std::unique_ptr createAuthTokenService(std::size_t maxThrottlerEntries) -{ - return std::make_unique(maxThrottlerEntries); -} - -static const Wt::Auth::SHA1HashFunction sha1Function; - -AuthTokenService::AuthTokenService(std::size_t maxThrottlerEntries) -: _loginThrottler {maxThrottlerEntries} +namespace Auth { -} -std::string -AuthTokenService::createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry) -{ - const std::string secret {Wt::WRandom::generateId(32)}; - const std::string secretHash {sha1Function.compute(secret, {})}; + std::unique_ptr createAuthTokenService(std::size_t maxThrottlerEntries) + { + return std::make_unique(maxThrottlerEntries); + } - auto transaction {session.createUniqueTransaction()}; + static const Wt::Auth::SHA1HashFunction sha1Function; - Database::User::pointer user {Database::User::getById(session, userId)}; - if (!user) - throw LmsException {"User deleted"}; + AuthTokenService::AuthTokenService(std::size_t maxThrottlerEntries) + : _loginThrottler {maxThrottlerEntries} + { + } - Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, expiry, user)}; + std::string + AuthTokenService::createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry) + { + const std::string secret {Wt::WRandom::generateId(32)}; + const std::string secretHash {sha1Function.compute(secret, {})}; - LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString(); + auto transaction {session.createUniqueTransaction()}; - if (user->getAuthTokensCount() >= 50) - Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime()); + Database::User::pointer user {Database::User::getById(session, userId)}; + if (!user) + throw Exception {"User deleted"}; - return secret; -} + Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, expiry, user)}; -static -std::optional -processAuthToken(Database::Session& session, const std::string& secret) -{ - const std::string secretHash {sha1Function.compute(secret, {})}; + LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString(); - auto transaction {session.createUniqueTransaction()}; + if (user->getAuthTokensCount() >= 50) + Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime()); - Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)}; - if (!authToken) - return std::nullopt; + return secret; + } - if (authToken->getExpiry() < Wt::WDateTime::currentDateTime()) + static + std::optional + processAuthToken(Database::Session& session, std::string_view secret) { - authToken.remove(); - return std::nullopt; - } + const std::string secretHash {sha1Function.compute(std::string {secret}, {})}; - LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!"; + auto transaction {session.createUniqueTransaction()}; - AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser().id(), authToken->getExpiry()}; - authToken.remove(); + Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)}; + if (!authToken) + return std::nullopt; - return res; -} + if (authToken->getExpiry() < Wt::WDateTime::currentDateTime()) + { + authToken.remove(); + return std::nullopt; + } + LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!"; -AuthTokenService::AuthTokenProcessResult -AuthTokenService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) -{ - // Do not waste too much resource on brute force attacks (optim) - { - std::shared_lock lock {_mutex}; + AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser().id(), authToken->getExpiry()}; + authToken.remove(); - if (_loginThrottler.isClientThrottled(clientAddress)) - return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled}; + return res; } - auto res {Auth::processAuthToken(session, tokenValue)}; + AuthTokenService::AuthTokenProcessResult + AuthTokenService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) { - std::unique_lock lock {_mutex}; + // Do not waste too much resource on brute force attacks (optim) + { + std::shared_lock lock {_mutex}; - if (_loginThrottler.isClientThrottled(clientAddress)) - return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled}; + if (_loginThrottler.isClientThrottled(clientAddress)) + return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled}; + } - if (!res) + auto res {Auth::processAuthToken(session, tokenValue)}; { - _loginThrottler.onBadClientAttempt(clientAddress); - return AuthTokenProcessResult {AuthTokenProcessResult::State::NotFound}; - } + std::unique_lock lock {_mutex}; + + if (_loginThrottler.isClientThrottled(clientAddress)) + return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled}; - _loginThrottler.onGoodClientAttempt(clientAddress); - return AuthTokenProcessResult {AuthTokenProcessResult::State::Found, std::move(*res)}; + if (!res) + { + _loginThrottler.onBadClientAttempt(clientAddress); + return AuthTokenProcessResult {AuthTokenProcessResult::State::Denied}; + } + + _loginThrottler.onGoodClientAttempt(clientAddress); + onUserAuthenticated(session, res->userId); + return AuthTokenProcessResult {AuthTokenProcessResult::State::Granted, std::move(*res)}; + } } -} + void + AuthTokenService::clearAuthTokens(Database::Session& session, Database::IdType userId) + { + auto transaction {session.createUniqueTransaction()}; -} // namespace Auth + Database::User::pointer user {Database::User::getById(session, userId)}; + if (!user) + throw Exception {"User deleted"}; + user.modify()->clearAuthTokens(); + } + +} // namespace Auth diff --git a/src/libs/auth/impl/AuthTokenService.hpp b/src/libs/auth/impl/AuthTokenService.hpp index c63f8193..92dff681 100644 --- a/src/libs/auth/impl/AuthTokenService.hpp +++ b/src/libs/auth/impl/AuthTokenService.hpp @@ -19,8 +19,10 @@ #pragma once -#include "auth/IAuthTokenService.hpp" +#include +#include "auth/IAuthTokenService.hpp" +#include "AuthServiceBase.hpp" #include "LoginThrottler.hpp" namespace Database @@ -28,13 +30,11 @@ namespace Database class Session; } - -namespace Auth { - - class AuthTokenService : public IAuthTokenService +namespace Auth +{ + class AuthTokenService : public IAuthTokenService, public AuthServiceBase { public: - AuthTokenService(std::size_t maxThrottlerEntries); AuthTokenService(const AuthTokenService&) = delete; @@ -42,14 +42,12 @@ namespace Auth { AuthTokenService(AuthTokenService&&) = delete; AuthTokenService& operator=(AuthTokenService&&) = delete; - AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) override; - std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) override; - private: + AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override; + std::string createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry) override; + void clearAuthTokens(Database::Session& session, Database::IdType userId) override; - std::shared_timed_mutex _mutex; - LoginThrottler _loginThrottler; + std::shared_mutex _mutex; + LoginThrottler _loginThrottler; }; - } - diff --git a/src/libs/auth/impl/EnvService.cpp b/src/libs/auth/impl/EnvService.cpp new file mode 100644 index 00000000..5ca529da --- /dev/null +++ b/src/libs/auth/impl/EnvService.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "auth/IEnvService.hpp" + +#include "auth/Types.hpp" +#include "http-headers/HttpHeadersEnvService.hpp" + +namespace Auth +{ + std::unique_ptr + createEnvService(std::string_view backendName) + { + if (backendName == "http-headers") + return std::make_unique(); + + throw Exception {"Authentication backend '" + std::string {backendName} + "' is not supported!"}; + } +} diff --git a/src/libs/auth/impl/PasswordService.cpp b/src/libs/auth/impl/PasswordService.cpp deleted file mode 100644 index 0c58b410..00000000 --- a/src/libs/auth/impl/PasswordService.cpp +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2019 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "PasswordService.hpp" - -#include -#include -#include - -#include "database/Session.hpp" -#include "utils/Exception.hpp" -#include "utils/Logger.hpp" -#ifdef LMS_SUPPORT_PAM -#include "pam/PAM.hpp" -#endif - -namespace Auth { - -std::unique_ptr createPasswordService(std::size_t maxThrottlerEntries) -{ - return std::make_unique(maxThrottlerEntries); -} - -PasswordService::PasswordService(std::size_t maxThrottlerEntries) -: _loginThrottler{maxThrottlerEntries} -{ -} - -bool -PasswordService::isAuthModeSupported(Database::User::AuthMode authMode) const -{ - switch (authMode) - { - case Database::User::AuthMode::Internal: - return true; - - case Database::User::AuthMode::PAM: -#ifdef LMS_SUPPORT_PAM - return true; -#else - return false; -#endif - } - return false; -} - -static -bool -checkUserPassword(Database::Session& session, const std::string& loginName, const std::string& password) -{ - Database::User::AuthMode authMode; - Database::User::PasswordHash passwordHash; - { - auto transaction {session.createSharedTransaction()}; - - const Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; - if (!user) - return false; - - authMode = user->getAuthMode(); - passwordHash = user->getPasswordHash(); - } - - switch (authMode) - { - case Database::User::AuthMode::Internal: - { - LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'"; - const Wt::Auth::BCryptHashFunction hashFunc {7}; // TODO parametrize this - return hashFunc.verify(password, passwordHash.salt, passwordHash.hash); - } - - case Database::User::AuthMode::PAM: -#ifdef LMS_SUPPORT_PAM - return PAM::checkUserPassword(loginName, password); -#else - return false; -#endif - } - - return false; -} - -PasswordService::PasswordCheckResult -PasswordService::checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) -{ - // Do not waste too much resource on brute force attacks (optim) - { - std::shared_lock lock {_mutex}; - - if (_loginThrottler.isClientThrottled(clientAddress)) - return PasswordCheckResult::Throttled; - } - - const bool match {Auth::checkUserPassword(session, loginName, password)}; - { - std::unique_lock lock {_mutex}; - - if (_loginThrottler.isClientThrottled(clientAddress)) - return PasswordCheckResult::Throttled; - - if (match) - { - _loginThrottler.onGoodClientAttempt(clientAddress); - return PasswordCheckResult::Match; - } - else - { - _loginThrottler.onBadClientAttempt(clientAddress); - return PasswordCheckResult::Mismatch; - } - } -} - -Database::User::PasswordHash -PasswordService::hashPassword(const std::string& password) const -{ - const std::string salt {Wt::WRandom::generateId(32)}; - - const Wt::Auth::BCryptHashFunction hashFunc {6}; - return {salt, hashFunc.compute(password, salt)}; -} - -bool -PasswordService::evaluatePasswordStrength(const std::string& loginName, const std::string& password) const -{ - Wt::Auth::PasswordStrengthValidator validator; - validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4); - validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4); - validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4); - validator.setMinimumLength(Wt::Auth::PasswordStrengthType::ThreeCharClass, 4); - validator.setMinimumLength(Wt::Auth::PasswordStrengthType::FourCharClass, 4); - validator.setMinimumPassPhraseWords(1); - validator.setMinimumMatchLength(3); - - return validator.evaluateStrength(password, loginName, "").isValid(); -} - -} // namespace Auth - diff --git a/src/libs/auth/impl/PasswordService.hpp b/src/libs/auth/impl/PasswordService.hpp deleted file mode 100644 index ddbb7e31..00000000 --- a/src/libs/auth/impl/PasswordService.hpp +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2019 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#pragma once - -#include -#include "auth/IPasswordService.hpp" - -#include "LoginThrottler.hpp" - -namespace Database -{ - class Session; -} - - -namespace Auth { - - class PasswordService : public IPasswordService - { - public: - - PasswordService(std::size_t maxThrottlerEntries); - - PasswordService(const PasswordService&) = delete; - PasswordService& operator=(const PasswordService&) = delete; - PasswordService(PasswordService&&) = delete; - PasswordService& operator=(PasswordService&&) = delete; - - private: - - bool isAuthModeSupported(Database::User::AuthMode authMode) const override; - PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) override; - Database::User::PasswordHash hashPassword(const std::string& password) const override; - bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const override; - - std::shared_timed_mutex _mutex; - LoginThrottler _loginThrottler; - }; - -} diff --git a/src/libs/auth/impl/PasswordServiceBase.cpp b/src/libs/auth/impl/PasswordServiceBase.cpp new file mode 100644 index 00000000..f1d53f6d --- /dev/null +++ b/src/libs/auth/impl/PasswordServiceBase.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "PasswordServiceBase.hpp" + +#include +#include + +#include "internal/InternalPasswordService.hpp" +#ifdef LMS_SUPPORT_PAM +#include "pam/PAMPasswordService.hpp" +#endif // LMS_SUPPORT_PAM + +#include "auth/Types.hpp" +#include "database/Session.hpp" +#include "database/User.hpp" +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +namespace Auth +{ + + static const Wt::Auth::SHA1HashFunction sha1Function; + + std::unique_ptr + createPasswordService(std::string_view passwordAuthenticationBackend, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) + { + if (passwordAuthenticationBackend == "internal") + return std::make_unique(maxThrottlerEntries, authTokenService); +#ifdef LMS_SUPPORT_PAM + else if (passwordAuthenticationBackend == "pam") + return std::make_unique(maxThrottlerEntries, authTokenService); +#endif // LMS_SUPPORT_PAM + + throw Exception {"Authentication backend '" + std::string {passwordAuthenticationBackend} + "' is not supported!"}; + } + + PasswordServiceBase::PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) + : _loginThrottler {maxThrottlerEntries} + , _authTokenService {authTokenService} + { + } + + PasswordServiceBase::CheckResult + PasswordServiceBase::checkUserPassword(Database::Session& session, + const boost::asio::ip::address& clientAddress, + std::string_view loginName, + std::string_view password) + { + LMS_LOG(AUTH, DEBUG) << "Checking password for user '" << loginName << "'"; + + // Do not waste too much resource on brute force attacks (optim) + { + std::shared_lock lock {_mutex}; + + if (_loginThrottler.isClientThrottled(clientAddress)) + return {CheckResult::State::Throttled}; + } + + const bool match {checkUserPassword(session, loginName, password)}; + { + std::unique_lock lock {_mutex}; + + if (_loginThrottler.isClientThrottled(clientAddress)) + return {CheckResult::State::Throttled}; + + if (match) + { + _loginThrottler.onGoodClientAttempt(clientAddress); + + const Database::IdType userId {getOrCreateUser(session, loginName)}; + onUserAuthenticated(session, userId); + return {CheckResult::State::Granted, userId}; + } + else + { + _loginThrottler.onBadClientAttempt(clientAddress); + return {CheckResult::State::Denied}; + } + } + } +} // namespace Auth + diff --git a/src/libs/auth/impl/PasswordServiceBase.hpp b/src/libs/auth/impl/PasswordServiceBase.hpp new file mode 100644 index 00000000..8b5bc3dd --- /dev/null +++ b/src/libs/auth/impl/PasswordServiceBase.hpp @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "auth/IPasswordService.hpp" +#include "AuthServiceBase.hpp" +#include "LoginThrottler.hpp" + +namespace Database +{ + class Session; +} + +namespace Auth +{ + + class PasswordServiceBase : public IPasswordService, public AuthServiceBase + { + public: + PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); + + PasswordServiceBase(const PasswordServiceBase&) = delete; + PasswordServiceBase& operator=(const PasswordServiceBase&) = delete; + PasswordServiceBase(PasswordServiceBase&&) = delete; + PasswordServiceBase& operator=(PasswordServiceBase&&) = delete; + + protected: + IAuthTokenService& getAuthTokenService() { return _authTokenService; } + + private: + virtual bool checkUserPassword(Database::Session& session, + std::string_view loginName, + std::string_view password) = 0; + + CheckResult checkUserPassword(Database::Session& session, + const boost::asio::ip::address& clientAddress, + std::string_view loginName, + std::string_view password) override; + + std::shared_mutex _mutex; + LoginThrottler _loginThrottler; + IAuthTokenService& _authTokenService; + }; + +} // namespace Auth diff --git a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp new file mode 100644 index 00000000..4d24085a --- /dev/null +++ b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "HttpHeadersEnvService.hpp" + +#include + +#include "utils/IConfig.hpp" +#include "utils/Logger.hpp" +#include "utils/Service.hpp" + +namespace Auth +{ + + HttpHeadersEnvService::HttpHeadersEnvService() + : _fieldName {Service::get()->getString("http-headers-field-name", "X-Forwarded-User")} + { + LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'"; + } + + HttpHeadersEnvService::CheckResult + HttpHeadersEnvService::processEnv(Database::Session& session, const Wt::WEnvironment& env) + { + const std::string loginName { env.headerValue(_fieldName)}; + if (loginName.empty()) + return {CheckResult::State::Denied}; + + LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header"; + + const Database::IdType userId {getOrCreateUser(session, loginName)}; + onUserAuthenticated(session, userId); + return {CheckResult::State::Granted, userId}; + } + +} // namespace Auth + diff --git a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp new file mode 100644 index 00000000..2f74d0de --- /dev/null +++ b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "auth/IEnvService.hpp" +#include "AuthServiceBase.hpp" + +namespace Auth +{ + class HttpHeadersEnvService : public IEnvService, public AuthServiceBase + { + public: + HttpHeadersEnvService(); + + private: + CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) override; + + std::string _fieldName; + }; + +} // namespace Auth + diff --git a/src/libs/auth/impl/internal/InternalPasswordService.cpp b/src/libs/auth/impl/internal/InternalPasswordService.cpp new file mode 100644 index 00000000..a662a76f --- /dev/null +++ b/src/libs/auth/impl/internal/InternalPasswordService.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "InternalPasswordService.hpp" +#include + +#include "auth/IAuthTokenService.hpp" +#include "auth/Types.hpp" +#include "database/Session.hpp" +#include "database/User.hpp" +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" + +namespace Auth +{ + InternalPasswordService::InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) + : PasswordServiceBase {maxThrottlerEntries, authTokenService} + { + _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4); + _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4); + _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4); + _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::ThreeCharClass, 4); + _validator.setMinimumLength(Wt::Auth::PasswordStrengthType::FourCharClass, 4); + _validator.setMinimumPassPhraseWords(1); + _validator.setMinimumMatchLength(3); + } + + bool + InternalPasswordService::checkUserPassword(Database::Session& session, + std::string_view loginName, + std::string_view password) + { + LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'"; + + Database::User::PasswordHash passwordHash; + { + auto transaction {session.createSharedTransaction()}; + + const Database::User::pointer user {Database::User::getByLoginName(session, loginName)}; + if (!user) + { + LMS_LOG(AUTH, DEBUG) << "hashing random stuff"; + // hash random stuff here to waste some time + hashRandomPassword(); + return false; + } + + // Don't allow users being created or coming from other backends + passwordHash = user->getPasswordHash(); + if (passwordHash.salt.empty() || passwordHash.hash.empty()) + { + // hash random stuff here to waste some time + hashRandomPassword(); + return false; + } + } + + return _hashFunc.verify(std::string {password}, std::string {passwordHash.salt}, std::string {passwordHash.hash}); + } + + bool + InternalPasswordService::canSetPasswords() const + { + return true; + } + + bool + InternalPasswordService::isPasswordSecureEnough(std::string_view loginName, std::string_view password) const + { + return _validator.evaluateStrength(std::string {password}, std::string {loginName}, "").isValid(); + } + + void + InternalPasswordService::setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) + { + const Database::User::PasswordHash passwordHash {hashPassword(newPassword)}; + + auto transaction {session.createUniqueTransaction()}; + + const Database::User::pointer user {Database::User::getById(session, userId)}; + if (!user) + throw Exception {"User not found!"}; + + if (!isPasswordSecureEnough(user->getLoginName(), newPassword)) + throw PasswordTooWeakException {}; + + user.modify()->setPasswordHash(passwordHash); + getAuthTokenService().clearAuthTokens(session, userId); + } + + Database::User::PasswordHash + InternalPasswordService::hashPassword(std::string_view password) const + { + const std::string salt {Wt::WRandom::generateId(32)}; + + return {salt, _hashFunc.compute(std::string {password}, salt)}; + } + + void + InternalPasswordService::hashRandomPassword() const + { + hashPassword(Wt::WRandom::generateId(32)); + } + +} // namespace Auth + diff --git a/src/libs/auth/impl/internal/InternalPasswordService.hpp b/src/libs/auth/impl/internal/InternalPasswordService.hpp new file mode 100644 index 00000000..efcc9e06 --- /dev/null +++ b/src/libs/auth/impl/internal/InternalPasswordService.hpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "database/User.hpp" +#include "PasswordServiceBase.hpp" +#include "LoginThrottler.hpp" + +namespace Auth +{ + class IAuthTokenService; + + class InternalPasswordService : public PasswordServiceBase + { + public: + InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); + + private: + + bool checkUserPassword(Database::Session& session, + std::string_view loginName, + std::string_view password) override; + + bool canSetPasswords() const override; + bool isPasswordSecureEnough(std::string_view loginName, std::string_view password) const override; + void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) override; + + Database::User::PasswordHash hashPassword(std::string_view password) const; + void hashRandomPassword() const; + + const Wt::Auth::BCryptHashFunction _hashFunc {7}; // TODO parametrize this + Wt::Auth::PasswordStrengthValidator _validator; + }; + +} diff --git a/src/libs/auth/impl/pam/PAM.cpp b/src/libs/auth/impl/pam/PAM.cpp deleted file mode 100644 index c0bf6200..00000000 --- a/src/libs/auth/impl/pam/PAM.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2020 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "PAM.hpp" - -#include - -#include "utils/Exception.hpp" -#include "utils/Logger.hpp" - -#include - -namespace Auth::PAM -{ - -class PAMError -{ - public: - PAMError(std::string_view msg, pam_handle_t *pamh, int err) - { - _errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err); - } - - std::string_view message() const { return _errorMsg; } - - private: - std::string _errorMsg; -}; - -class PAMContext -{ - public: - PAMContext(std::string_view loginName) - { - int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)}; - if (err != PAM_SUCCESS) - throw PAMError {"start failed", _pamh, err}; - } - - ~PAMContext() - { - int err {pam_end(_pamh, 0)}; - if (err != PAM_SUCCESS) - LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err); - } - - void authenticate(std::string_view password) - { - AuthenticateConvContext authContext {password}; - ScopedConvContextSetter scopedContext {*this, authContext}; - - int err {pam_authenticate(_pamh, 0)}; - if (err != PAM_SUCCESS) - throw PAMError {"authenticate failed", _pamh, err}; - } - - void validateAccount() - { - int err {pam_acct_mgmt(_pamh, PAM_SILENT)}; - if (err != PAM_SUCCESS) - throw PAMError {"acct_mgmt failed", _pamh, err}; - } - - private: - - class ConvContext - { - public: - virtual ~ConvContext() = default; - }; - - class AuthenticateConvContext final : public ConvContext - { - public: - AuthenticateConvContext(std::string_view password) : _password {password} {} - - std::string_view getPassword() const { return _password; } - - private: - std::string_view _password; - }; - - class ScopedConvContextSetter - { - public: - ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext) - : _pamContext {pamContext} - { - _pamContext._convContext = &convContext; - } - - ~ScopedConvContextSetter() - { - _pamContext._convContext = nullptr; - } - - ScopedConvContextSetter(const ScopedConvContextSetter&) = delete; - ScopedConvContextSetter(ScopedConvContextSetter&&) = delete; - ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete; - ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete; - - private: - PAMContext& _pamContext; - }; - - - static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData) - { - if (msgCount < 1) - return PAM_CONV_ERR; - if (!resps || !msgs || !userData) - return PAM_CONV_ERR; - - PAMContext& context {*static_cast(userData)}; - - AuthenticateConvContext* authenticateContext = dynamic_cast(context._convContext); - if (!authenticateContext) - { - LMS_LOG(AUTH, ERROR) << "Unexpected conv!"; - return PAM_CONV_ERR; - } - - // Only expect a PAM_PROMPT_ECHO_OFF msg - if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF) - { - LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount; - return PAM_CONV_ERR; - } - - pam_response* response {static_cast(malloc(sizeof(pam_response)))}; - if (!response) - return PAM_CONV_ERR; - - response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str()); - - *resps = response; - return PAM_SUCCESS; - } - - ConvContext* _convContext {}; - pam_conv _conv {&PAMContext::conv, this}; - pam_handle_t *_pamh {}; - -}; - -bool -checkUserPassword(const std::string& loginName, const std::string& password) -{ - try - { - LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'"; - PAMContext pamContext {loginName}; - - pamContext.authenticate(password); - pamContext.validateAccount(); - - return true; - } - catch (const PAMError& error) - { - LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message(); - return false; - } -} - -} // namespace Auth::PAM - diff --git a/src/libs/auth/impl/pam/PAMPasswordService.cpp b/src/libs/auth/impl/pam/PAMPasswordService.cpp new file mode 100644 index 00000000..a9e91569 --- /dev/null +++ b/src/libs/auth/impl/pam/PAMPasswordService.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "PAMPasswordService.hpp" + +#ifndef LMS_SUPPORT_PAM +#error "Should not compile this" +#endif + +#include +#include + +#include "auth/Types.hpp" +#include "database/Session.hpp" +#include "utils/Logger.hpp" + +namespace Auth +{ + class PAMError + { + public: + PAMError(std::string_view msg, pam_handle_t *pamh, int err) + { + _errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err); + } + + std::string_view message() const { return _errorMsg; } + + private: + std::string _errorMsg; + }; + + class PAMContext + { + public: + PAMContext(std::string_view loginName) + { + int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)}; + if (err != PAM_SUCCESS) + throw PAMError {"start failed", _pamh, err}; + } + + ~PAMContext() + { + int err {pam_end(_pamh, 0)}; + if (err != PAM_SUCCESS) + LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err); + } + + void authenticate(std::string_view password) + { + AuthenticateConvContext authContext {password}; + ScopedConvContextSetter scopedContext {*this, authContext}; + + int err {pam_authenticate(_pamh, 0)}; + if (err != PAM_SUCCESS) + throw PAMError {"authenticate failed", _pamh, err}; + } + + void validateAccount() + { + int err {pam_acct_mgmt(_pamh, PAM_SILENT)}; + if (err != PAM_SUCCESS) + throw PAMError {"acct_mgmt failed", _pamh, err}; + } + + private: + + class ConvContext + { + public: + virtual ~ConvContext() = default; + }; + + class AuthenticateConvContext final : public ConvContext + { + public: + AuthenticateConvContext(std::string_view password) : _password {password} {} + + std::string_view getPassword() const { return _password; } + + private: + std::string_view _password; + }; + + class ScopedConvContextSetter + { + public: + ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext) + : _pamContext {pamContext} + { + _pamContext._convContext = &convContext; + } + + ~ScopedConvContextSetter() + { + _pamContext._convContext = nullptr; + } + + ScopedConvContextSetter(const ScopedConvContextSetter&) = delete; + ScopedConvContextSetter(ScopedConvContextSetter&&) = delete; + ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete; + ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete; + + private: + PAMContext& _pamContext; + }; + + + static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData) + { + if (msgCount < 1) + return PAM_CONV_ERR; + if (!resps || !msgs || !userData) + return PAM_CONV_ERR; + + PAMContext& context {*static_cast(userData)}; + + AuthenticateConvContext* authenticateContext = dynamic_cast(context._convContext); + if (!authenticateContext) + { + LMS_LOG(AUTH, ERROR) << "Unexpected conv!"; + return PAM_CONV_ERR; + } + + // Only expect a PAM_PROMPT_ECHO_OFF msg + if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF) + { + LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount; + return PAM_CONV_ERR; + } + + pam_response* response {static_cast(malloc(sizeof(pam_response)))}; + if (!response) + return PAM_CONV_ERR; + + response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str()); + + *resps = response; + return PAM_SUCCESS; + } + + ConvContext* _convContext {}; + pam_conv _conv {&PAMContext::conv, this}; + pam_handle_t *_pamh {}; + + }; + + bool + PAMPasswordService::checkUserPassword(Database::Session& /*session*/, std::string_view loginName, std::string_view password) + { + try + { + LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'"; + PAMContext pamContext {loginName}; + + pamContext.authenticate(password); + pamContext.validateAccount(); + + return true; + } + catch (const PAMError& error) + { + LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message(); + return false; + } + } + + bool + PAMPasswordService::canSetPasswords() const + { + return false; + } + + bool + PAMPasswordService::isPasswordSecureEnough(std::string_view, std::string_view) const + { + throw NotImplementedException {}; + } + + void + PAMPasswordService::setPassword(Database::Session&, Database::IdType, std::string_view) + { + throw NotImplementedException {}; + } + +} // namespace Auth + diff --git a/src/libs/auth/impl/pam/PAMPasswordService.hpp b/src/libs/auth/impl/pam/PAMPasswordService.hpp new file mode 100644 index 00000000..400e0b69 --- /dev/null +++ b/src/libs/auth/impl/pam/PAMPasswordService.hpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +#include "PasswordServiceBase.hpp" + +namespace Auth +{ + class PAMPasswordService: public PasswordServiceBase + { + public: + using PasswordServiceBase::PasswordServiceBase; + + private: + + bool checkUserPassword(Database::Session& session, + std::string_view loginName, + std::string_view password) override; + + bool canSetPasswords() const override; + bool isPasswordSecureEnough(std::string_view loginName, + std::string_view password) const override; + void setPassword(Database::Session& session, + Database::IdType userId, + std::string_view newPassword) override; + }; +} diff --git a/src/libs/auth/include/auth/IAuthTokenService.hpp b/src/libs/auth/include/auth/IAuthTokenService.hpp index 1018b674..44744198 100644 --- a/src/libs/auth/include/auth/IAuthTokenService.hpp +++ b/src/libs/auth/include/auth/IAuthTokenService.hpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -31,11 +32,11 @@ namespace Database { class Session; + class User; } - -namespace Auth { - +namespace Auth +{ class IAuthTokenService { public: @@ -46,9 +47,9 @@ namespace Auth { { enum class State { - Found, + Granted, Throttled, - NotFound, + Denied, }; struct AuthTokenInfo @@ -57,16 +58,17 @@ namespace Auth { Wt::WDateTime expiry; }; - State state {State::NotFound}; + State state {State::Denied}; std::optional authTokenInfo {}; }; - // Removed if found - virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) = 0; - virtual std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) = 0; + // Provided token is only accepted once + virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0; + + // Returns a one time token + virtual std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) = 0; + virtual void clearAuthTokens(Database::Session& session, Database::IdType userid) = 0; }; std::unique_ptr createAuthTokenService(std::size_t maxThrottlerEntryCount); - } - diff --git a/src/libs/auth/include/auth/IEnvService.hpp b/src/libs/auth/include/auth/IEnvService.hpp new file mode 100644 index 00000000..336432e3 --- /dev/null +++ b/src/libs/auth/include/auth/IEnvService.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include + +#include "database/Types.hpp" + +namespace Database +{ + class Session; +} + +namespace Wt +{ + class WEnvironment; +} + +namespace Auth +{ + class IEnvService + { + public: + virtual ~IEnvService() = default; + + // Auth Token services + struct CheckResult + { + enum class State + { + Granted, + Denied, + Throttled, + }; + + State state {State::Denied}; + std::optional userId {}; + }; + + virtual CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) = 0; + }; + + std::unique_ptr createEnvService(std::string_view backendName); +} // namespace Auth diff --git a/src/libs/auth/include/auth/IPasswordService.hpp b/src/libs/auth/include/auth/IPasswordService.hpp index 1608c972..896c4156 100644 --- a/src/libs/auth/include/auth/IPasswordService.hpp +++ b/src/libs/auth/include/auth/IPasswordService.hpp @@ -19,44 +19,59 @@ #pragma once -#include -#include +#include -#include +#include +#include -#include "database/User.hpp" +#include +#include "auth/Types.hpp" #include "database/Types.hpp" namespace Database { class Session; + class User; } +namespace Auth +{ -namespace Auth { + class IAuthTokenService; class IPasswordService { public: - virtual ~IPasswordService() = default; - // Password services - enum class PasswordCheckResult + struct CheckResult { - Match, - Mismatch, - Throttled, + enum class State + { + Granted, + Denied, + Throttled, + }; + State state {State::Denied}; + std::optional userId {}; + std::optional expiry {}; }; + virtual CheckResult checkUserPassword(Database::Session& session, + const boost::asio::ip::address& clientAddress, + std::string_view loginName, + std::string_view password) = 0; - virtual bool isAuthModeSupported(Database::User::AuthMode authMode) const = 0; + class PasswordTooWeakException : public Auth::Exception + { + public: + PasswordTooWeakException() : Auth::Exception {"Password too weak"} {} + }; - virtual PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) = 0; - virtual Database::User::PasswordHash hashPassword(const std::string& password) const = 0; - virtual bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const = 0; + virtual bool canSetPasswords() const = 0; + virtual bool isPasswordSecureEnough(std::string_view username, std::string_view password) const = 0; + virtual void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) = 0; }; - std::unique_ptr createPasswordService(std::size_t maxThrottlerEntryCount); - + std::unique_ptr createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService); } diff --git a/src/libs/auth/include/auth/Types.hpp b/src/libs/auth/include/auth/Types.hpp new file mode 100644 index 00000000..9b79bab7 --- /dev/null +++ b/src/libs/auth/include/auth/Types.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include "utils/Exception.hpp" + +namespace Auth +{ + class Exception : public ::LmsException + { + using LmsException::LmsException; + }; + + class NotImplementedException : public Exception + { + public: + NotImplementedException() : Auth::Exception {"Not implemented"} {} + }; +} + diff --git a/src/libs/cover/CMakeLists.txt b/src/libs/cover/CMakeLists.txt index b4643219..a675db76 100644 --- a/src/libs/cover/CMakeLists.txt +++ b/src/libs/cover/CMakeLists.txt @@ -9,6 +9,7 @@ target_include_directories(lmscover INTERFACE target_include_directories(lmscover PRIVATE include + impl ) target_link_libraries(lmscover PRIVATE @@ -39,7 +40,5 @@ else () message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided") endif() -target_include_directories(lmscover PRIVATE impl) - install(TARGETS lmscover DESTINATION lib) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 005bea2e..95dd0e16 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "utils/Exception.hpp" #include "utils/Logger.hpp" @@ -40,17 +41,16 @@ namespace Database { -#define LMS_DATABASE_VERSION 28 + using Version = std::size_t; + static constexpr Version LMS_DATABASE_VERSION {29}; -using Version = std::size_t; - -class VersionInfo -{ - public: - using pointer = Wt::Dbo::ptr; + class VersionInfo + { + public: + using pointer = Wt::Dbo::ptr; - static VersionInfo::pointer getOrCreate(Session& session) - { + static VersionInfo::pointer getOrCreate(Session& session) + { session.checkUniqueLocked(); pointer versionInfo {session.getDboSession().find()}; @@ -270,7 +270,7 @@ CREATE TABLE "user_backup" ( else if (version == 24) { // User's AuthMode - _session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultAuthMode)) + ")"); + _session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(/*User::defaultAuthMode*/0)) + ")"); } else if (version == 25) { @@ -290,6 +290,31 @@ CREATE TABLE "user_backup" ( // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); } + else if (version == 28) + { + // Drop Auth mode + _session.execute(R"( +CREATE TABLE "user_backup" ( + "id" integer primary key autoincrement, + "version" integer not null, + "type" integer not null, + "login_name" text not null, + "password_salt" text not null, + "password_hash" text not null, + "last_login" text, + "subsonic_transcode_enable" boolean not null, + "subsonic_transcode_format" integer not null, + "subsonic_transcode_bitrate" integer not null, + "subsonic_artist_list_mode" integer not null, + "ui_theme" integer not null, + "cur_playing_track_pos" integer not null, + "repeat_all" boolean not null, + "radio" boolean not null +))"); + _session.execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user"); + _session.execute("DROP TABLE user"); + _session.execute("ALTER TABLE user_backup RENAME TO user"); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; @@ -329,46 +354,28 @@ enum class OwnedLock Unique, }; -static thread_local std::map lockDebug; - -UniqueTransaction::UniqueTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session) +UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session) : _lock {mutex}, _transaction {session} { - assert(lockDebug[_lock.mutex()] == OwnedLock::None); - lockDebug[_lock.mutex()] = OwnedLock::Unique; -} - -UniqueTransaction::~UniqueTransaction() -{ - assert(lockDebug[_lock.mutex()] == OwnedLock::Unique); - lockDebug[_lock.mutex()] = OwnedLock::None; } -SharedTransaction::SharedTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session) +SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session) : _lock {mutex}, _transaction {session} { - assert(lockDebug[_lock.mutex()] == OwnedLock::None); - lockDebug[_lock.mutex()] = OwnedLock::Shared; -} - -SharedTransaction::~SharedTransaction() -{ - assert(lockDebug[_lock.mutex()] == OwnedLock::Shared); - lockDebug[_lock.mutex()] = OwnedLock::None; } void Session::checkUniqueLocked() { - assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique); +// assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique); } void Session::checkSharedLocked() { - assert(lockDebug[&_db.getMutex()] != OwnedLock::None); +// assert(lockDebug[&_db.getMutex()] != OwnedLock::None); } UniqueTransaction diff --git a/src/libs/database/impl/StringViewTraits.hpp b/src/libs/database/impl/StringViewTraits.hpp new file mode 100644 index 00000000..bfbd65be --- /dev/null +++ b/src/libs/database/impl/StringViewTraits.hpp @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +namespace Wt::Dbo +{ + template<> + struct sql_value_traits + { + static void bind(std::string_view str, SqlStatement *statement, int column, int /* size */) + { + statement->bind(column, std::string {str}); + } + }; +} + diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index df761cff..055fcac0 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -25,6 +25,7 @@ #include "database/Track.hpp" #include "database/TrackList.hpp" #include "utils/Logger.hpp" +#include "StringViewTraits.hpp" namespace Database { @@ -70,7 +71,7 @@ AuthToken::getByValue(Session& session, const std::string& value) static const std::string playedListName {"__played_tracks__"}; static const std::string queuedListName {"__queued_tracks__"}; -User::User(const std::string& loginName) +User::User(std::string_view loginName) : _loginName {loginName} { } @@ -93,8 +94,16 @@ User::getDemo(Session& session) return res; } +std::size_t +User::getCount(Session& session) +{ + session.checkSharedLocked(); + + return session.getDboSession().query("SELECT COUNT(*) FROM user"); +} + User::pointer -User::create(Session& session, const std::string& loginName) +User::create(Session& session, std::string_view loginName) { session.checkUniqueLocked(); @@ -115,7 +124,7 @@ User::getById(Session& session, IdType id) } User::pointer -User::getByLoginName(Session& session, const std::string& name) +User::getByLoginName(Session& session, std::string_view name) { return session.getDboSession().find() .where("login_name = ?").bind(name); diff --git a/src/libs/database/include/database/Db.hpp b/src/libs/database/include/database/Db.hpp index ebece1a6..bdcd20b3 100644 --- a/src/libs/database/include/database/Db.hpp +++ b/src/libs/database/include/database/Db.hpp @@ -20,10 +20,11 @@ #pragma once #include -#include #include +#include "utils/RecursiveSharedMutex.hpp" + namespace Database { class Session; @@ -44,7 +45,7 @@ class Db private: friend class Session; - std::shared_mutex& getMutex() { return _sharedMutex; } + RecursiveSharedMutex& getMutex() { return _sharedMutex; } Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; } class ScopedConnection @@ -88,7 +89,7 @@ class Db void executeSql(const std::string& sql); - std::shared_mutex _sharedMutex; + RecursiveSharedMutex _sharedMutex; std::unique_ptr _connectionPool; std::mutex _tlsSessionsMutex; diff --git a/src/libs/database/include/database/Session.hpp b/src/libs/database/include/database/Session.hpp index db14ba77..6378c0a3 100644 --- a/src/libs/database/include/database/Session.hpp +++ b/src/libs/database/include/database/Session.hpp @@ -19,74 +19,68 @@ #pragma once -#include -#include #include -#include -#include +#include #include #include -namespace Database { +#include "utils/RecursiveSharedMutex.hpp" -class UniqueTransaction +namespace Database { - public: - ~UniqueTransaction(); - private: - friend class Session; - UniqueTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session); + class UniqueTransaction + { + private: + friend class Session; + UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session); - std::unique_lock _lock; - Wt::Dbo::Transaction _transaction; -}; + std::unique_lock _lock; + Wt::Dbo::Transaction _transaction; + }; -class SharedTransaction -{ - public: - ~SharedTransaction(); + class SharedTransaction + { + private: + friend class Session; + SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session); - private: - friend class Session; - SharedTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session); + std::shared_lock _lock; + Wt::Dbo::Transaction _transaction; + }; - std::shared_lock _lock; - Wt::Dbo::Transaction _transaction; -}; - -class Db; -class Session -{ - public: - Session (Db& database); + class Db; + class Session + { + public: + Session (Db& database); - Session(const Session&) = delete; - Session(Session&&) = delete; - Session& operator=(const Session&) = delete; - Session& operator=(Session&&) = delete; + Session(const Session&) = delete; + Session(Session&&) = delete; + Session& operator=(const Session&) = delete; + Session& operator=(Session&&) = delete; - [[nodiscard]] UniqueTransaction createUniqueTransaction(); - [[nodiscard]] SharedTransaction createSharedTransaction(); + [[nodiscard]] UniqueTransaction createUniqueTransaction(); + [[nodiscard]] SharedTransaction createSharedTransaction(); - void checkUniqueLocked(); - void checkSharedLocked(); + void checkUniqueLocked(); + void checkSharedLocked(); - void optimize(); + void optimize(); - void prepareTables(); // need to run only once at startup + void prepareTables(); // need to run only once at startup - Wt::Dbo::Session& getDboSession() { return _session; } + Wt::Dbo::Session& getDboSession() { return _session; } - private: - Session(std::shared_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool); + private: + Session(std::shared_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool); - void doDatabaseMigrationIfNeeded(); + void doDatabaseMigrationIfNeeded(); - Db& _db; - Wt::Dbo::Session _session; -}; + Db& _db; + Wt::Dbo::Session _session; + }; } // namespace Database diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index 576328d6..52932a88 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -20,6 +20,7 @@ #pragma once #include +#include #include #include @@ -100,12 +101,6 @@ class User : public Wt::Dbo::Dbo DEMO = 2, }; - enum class AuthMode - { - Internal = 0, - PAM = 1, - }; - struct PasswordHash { std::string salt; @@ -144,19 +139,19 @@ class User : public Wt::Dbo::Dbo static inline const Bitrate defaultSubsonicTranscodeBitrate {128000}; static inline const UITheme defaultUITheme {UITheme::Dark}; static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists}; - static inline const AuthMode defaultAuthMode {AuthMode::Internal}; User() = default; - User(const std::string& loginName); + User(std::string_view loginName); // utility - static pointer create(Session& session, const std::string& loginName); + static pointer create(Session& session, std::string_view loginName); static pointer getById(Session& session, IdType id); - static pointer getByLoginName(Session& session, const std::string& loginName); + static pointer getByLoginName(Session& session, std::string_view loginName); static std::vector getAll(Session& session); static pointer getDemo(Session& session); + static std::size_t getCount(Session& session); // accessors const std::string& getLoginName() const { return _loginName; } @@ -173,7 +168,6 @@ class User : public Wt::Dbo::Dbo void setSubsonicTranscodeBitrate(Bitrate bitrate); void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } void setRadio(bool val) { _radio = val; } - void setAuthMode(AuthMode mode) { _authMode = mode;} void setRepeatAll(bool val) { _repeatAll = val; } void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } void clearAuthTokens(); @@ -188,7 +182,6 @@ class User : public Wt::Dbo::Dbo std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } bool isRepeatAllSet() const { return _repeatAll; } bool isRadioSet() const { return _radio; } - AuthMode getAuthMode() const { return _authMode; } UITheme getUITheme() const { return _uiTheme; } SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } @@ -225,7 +218,6 @@ class User : public Wt::Dbo::Dbo Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); Wt::Dbo::field(a, _repeatAll, "repeat_all"); Wt::Dbo::field(a, _radio, "radio"); - Wt::Dbo::field(a, _authMode, "auth_mode"); Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user"); Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade); @@ -248,13 +240,12 @@ class User : public Wt::Dbo::Dbo SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode}; bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable}; AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat}; - int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate}; + int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate}; // User's dynamic data (UI) int _curPlayingTrackPos {}; // Current track position in queue bool _repeatAll {}; bool _radio {}; - AuthMode _authMode {defaultAuthMode}; Wt::Dbo::collection> _tracklists; Wt::Dbo::collection> _starredArtists; diff --git a/src/libs/scanner/impl/AcousticBrainzUtils.cpp b/src/libs/scanner/impl/AcousticBrainzUtils.cpp index b10cdd46..918b4d13 100644 --- a/src/libs/scanner/impl/AcousticBrainzUtils.cpp +++ b/src/libs/scanner/impl/AcousticBrainzUtils.cpp @@ -38,9 +38,9 @@ static std::string getJsonData(const UUID& mbid) { - static const std::string defaultAPIURL = "https://acousticbrainz.org/api/v1/"; + static constexpr std::string_view defaultAPIURL {"https://acousticbrainz.org/api/v1/"}; - const std::string url {Service::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"}; + const std::string url {std::string {Service::get()->getString("acousticbrainz-api-url", defaultAPIURL)} + std::string {mbid.getAsString()} + "/low-level"}; boost::asio::io_service ioService; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 610adf85..6c922a21 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -105,6 +105,15 @@ namespace StringUtils namespace API::Subsonic { +static +void +checkSetPasswordImplemented() +{ + Auth::IPasswordService* passwordService {Service::get()}; + if (!passwordService || !passwordService->canSetPasswords()) + throw NotImplementedGenericError {}; +} + static std::string makeNameFilesystemCompatible(const std::string& name) @@ -515,21 +524,31 @@ handleChangePassword(RequestContext& context) std::string username {getMandatoryParameterAs(context.parameters, "username")}; std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(context.parameters, "password"))}; - if (!Service::get()->evaluatePasswordStrength(username, password)) - throw PasswordTooWeakGenericError {}; + try + { + Database::IdType userId; + { + auto transaction {context.dbSession.createSharedTransaction()}; - const User::PasswordHash hash {Service::get()->hashPassword(password)}; + checkUserIsMySelfOrAdmin(context, username); - auto transaction {context.dbSession.createUniqueTransaction()}; + User::pointer user {User::getByLoginName(context.dbSession, username)}; + if (!user) + throw UserNotAuthorizedError {}; - checkUserIsMySelfOrAdmin(context, username); + userId = user.id(); + } - User::pointer user {User::getByLoginName(context.dbSession, username)}; - if (!user) + Service::get()->setPassword(context.dbSession, userId, password); + } + catch (Auth::IPasswordService::PasswordTooWeakException&) + { + throw PasswordTooWeakGenericError {}; + } + catch (Auth::Exception& authException) + { throw UserNotAuthorizedError {}; - - user.modify()->setPasswordHash(hash); - user.modify()->clearAuthTokens(); + } return Response::createOkResponse(context); } @@ -597,19 +616,40 @@ handleCreateUserRequest(RequestContext& context) std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(context.parameters, "password"))}; // Just ignore all the other fields as we don't handle them - if (!Service::get()->evaluatePasswordStrength(username, password)) - throw PasswordTooWeakGenericError {}; + Database::IdType userId; + { + auto transaction {context.dbSession.createUniqueTransaction()}; - const User::PasswordHash hash {Service::get()->hashPassword(password)}; + User::pointer user {User::getByLoginName(context.dbSession, username)}; + if (user) + throw UserAlreadyExistsGenericError {}; - auto transaction {context.dbSession.createUniqueTransaction()}; + user = User::create(context.dbSession, username); + userId = user.id(); + } - if (User::getByLoginName(context.dbSession, username) != User::pointer{}) - throw UserAlreadyExistsGenericError {}; + auto removeCreatedUser {[&]() + { + auto transaction {context.dbSession.createUniqueTransaction()}; + User::pointer user {User::getById(context.dbSession, userId)}; + if (user) + user.remove(); + }}; - User::pointer user {User::create(context.dbSession, username)}; - user.modify()->setAuthMode(User::AuthMode::Internal); - user.modify()->setPasswordHash(hash); + try + { + Service::get()->setPassword(context.dbSession, userId, password); + } + catch (const Auth::IPasswordService::PasswordTooWeakException&) + { + removeCreatedUser(); + throw PasswordTooWeakGenericError {}; + } + catch (const Auth::Exception& exception) + { + removeCreatedUser(); + throw UserNotAuthorizedError {}; + } return Response::createOkResponse(context); } @@ -1614,26 +1654,33 @@ handleUpdateUserRequest(RequestContext& context) std::string username {getMandatoryParameterAs(context.parameters, "username")}; std::optional password {getParameterAs(context.parameters, "password")}; - User::PasswordHash hash; - if (password) + Database::IdType userId; { - *password = decodePasswordIfNeeded(*password); - if (!Service::get()->evaluatePasswordStrength(username, *password)) - throw PasswordTooWeakGenericError {}; - - hash = Service::get()->hashPassword(*password); - } + auto transaction {context.dbSession.createSharedTransaction()}; - auto transaction {context.dbSession.createUniqueTransaction()}; + User::pointer user {User::getByLoginName(context.dbSession, username)}; + if (!user) + throw RequestedDataNotFoundError {}; - User::pointer user {User::getByLoginName(context.dbSession, username)}; - if (!user) - throw UserNotAuthorizedError {}; + userId = user.id(); + } if (password) { - user.modify()->setPasswordHash(hash); - user.modify()->clearAuthTokens(); + checkSetPasswordImplemented(); + + try + { + Service<::Auth::IPasswordService>()->setPassword(context.dbSession, userId, decodePasswordIfNeeded(*password)); + } + catch (const Auth::IPasswordService::PasswordTooWeakException&) + { + throw PasswordTooWeakGenericError {}; + } + catch (const Auth::Exception&) + { + throw UserNotAuthorizedError {}; + } } return Response::createOkResponse(context); @@ -1826,10 +1873,12 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, } using RequestHandlerFunc = std::function; +using CheckImplementedFunc = std::function; struct RequestEntryPointInfo { - RequestHandlerFunc func; - bool mustBeAdmin; + RequestHandlerFunc func; + bool mustBeAdmin; + CheckImplementedFunc checkFunc {}; }; static std::unordered_map requestEntryPoints @@ -1918,12 +1967,12 @@ static std::unordered_map requestEntryPoints {"addChatMessages", {handleNotImplemented, false}}, // User management - {"getUser", {handleGetUserRequest, false}}, + {"getUser", {handleGetUserRequest, false}}, {"getUsers", {handleGetUsersRequest, true}}, - {"createUser", {handleCreateUserRequest, true}}, + {"createUser", {handleCreateUserRequest, true, &checkSetPasswordImplemented}}, {"updateUser", {handleUpdateUserRequest, true}}, {"deleteUser", {handleDeleteUserRequest, true}}, - {"changePassword", {handleChangePassword, false}}, + {"changePassword", {handleChangePassword, false, &checkSetPasswordImplemented}}, // Bookmarks {"getBookmarks", {handleGetBookmarks, false}}, @@ -1975,15 +2024,17 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp Session& dbSession {_db.getTLSSession()}; - switch (Service::get()->checkUserPassword(dbSession, - boost::asio::ip::address::from_string(request.clientAddress()), - clientInfo.user, clientInfo.password)) + const Auth::IPasswordService::CheckResult checkResult {Service::get()->checkUserPassword(dbSession, + boost::asio::ip::address::from_string(request.clientAddress()), + clientInfo.user, clientInfo.password)}; + + switch (checkResult.state) { - case Auth::IPasswordService::PasswordCheckResult::Match: + case Auth::IPasswordService::CheckResult::State::Granted: break; - case Auth::IPasswordService::PasswordCheckResult::Mismatch: + case Auth::IPasswordService::CheckResult::State::Denied: throw WrongUsernameOrPasswordError {}; - case Auth::IPasswordService::PasswordCheckResult::Throttled: + case Auth::IPasswordService::CheckResult::State::Throttled: throw LoginThrottledGenericError {}; } @@ -1992,6 +2043,9 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp auto itEntryPoint {requestEntryPoints.find(requestPath)}; if (itEntryPoint != requestEntryPoints.end()) { + if (itEntryPoint->second.checkFunc) + itEntryPoint->second.checkFunc(); + if (itEntryPoint->second.mustBeAdmin) { auto transaction {dbSession.createSharedTransaction()}; diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 2917cbb1..39d6ee8d 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(lmsutils SHARED impl/NetAddress.cpp impl/Path.cpp impl/Random.cpp + impl/RecursiveSharedMutex.cpp impl/StreamLogger.cpp impl/String.cpp impl/UUID.cpp diff --git a/src/libs/utils/impl/Config.cpp b/src/libs/utils/impl/Config.cpp index ff8c4846..ba19cc0a 100644 --- a/src/libs/utils/impl/Config.cpp +++ b/src/libs/utils/impl/Config.cpp @@ -47,70 +47,62 @@ Config::Config(const std::filesystem::path& p) } } -std::string -Config::getString(const std::string& setting, const std::string& def, const std::unordered_set& allowedValues) +std::string_view +Config::getString(std::string_view setting, std::string_view def) { try { - std::string res {(const char*)_config.lookup(setting)}; - - if (!allowedValues.empty() && allowedValues.find(res) == std::cend(allowedValues)) - { - LMS_LOG(MAIN, ERROR) << "Invalid setting for '" << setting << "', using default value '" << def << "'"; - return def; - } - - return res; + return static_cast(_config.lookup(std::string {setting})); } - catch (std::exception &e) + catch (libconfig::ConfigException&) { return def; } } std::filesystem::path -Config::getPath(const std::string& setting, const std::filesystem::path& path) +Config::getPath(std::string_view setting, const std::filesystem::path& path) { try { - const char* res = _config.lookup(setting); + const char* res {_config.lookup(std::string {setting})}; return std::filesystem::path {std::string(res)}; } - catch (std::exception &e) + catch (libconfig::ConfigException&) { return path; } } unsigned long -Config::getULong(const std::string& setting, unsigned long def) +Config::getULong(std::string_view setting, unsigned long def) { try { - return static_cast(_config.lookup(setting)); + return static_cast(_config.lookup(std::string {setting})); } - catch (...) + catch (libconfig::ConfigException&) { return def; } } long -Config::getLong(const std::string& setting, long def) +Config::getLong(std::string_view setting, long def) { try { - return _config.lookup(setting); + return _config.lookup(std::string {setting}); } - catch (...) + catch (libconfig::ConfigException&) { return def; } } bool -Config::getBool(const std::string& setting, bool def) +Config::getBool(std::string_view setting, bool def) { try { - return _config.lookup(setting); + return _config.lookup(std::string {setting}); } - catch (...) + catch (libconfig::ConfigException&) { return def; } diff --git a/src/libs/utils/impl/Config.hpp b/src/libs/utils/impl/Config.hpp index e1e9600e..7dce0423 100644 --- a/src/libs/utils/impl/Config.hpp +++ b/src/libs/utils/impl/Config.hpp @@ -35,11 +35,11 @@ class Config final : public IConfig Config& operator=(Config&&) = delete; // Default values are returned in case of setting not found - std::string getString(const std::string& setting, const std::string& def = "", const std::unordered_set& allowedValues = {}) override; - std::filesystem::path getPath(const std::string& setting, const std::filesystem::path& def = std::filesystem::path()) override; - unsigned long getULong(const std::string& setting, unsigned long def = 0) override; - long getLong(const std::string& setting, long def = 0) override; - bool getBool(const std::string& setting, bool def = false) override; + std::string_view getString(std::string_view setting, std::string_view def = "") override; + std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override; + unsigned long getULong(std::string_view setting, unsigned long def = 0) override; + long getLong(std::string_view setting, long def = 0) override; + bool getBool(std::string_view setting, bool def = false) override; private: diff --git a/src/libs/utils/impl/RecursiveSharedMutex.cpp b/src/libs/utils/impl/RecursiveSharedMutex.cpp new file mode 100644 index 00000000..4adaa18a --- /dev/null +++ b/src/libs/utils/impl/RecursiveSharedMutex.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "utils/RecursiveSharedMutex.hpp" + +#include + +void +RecursiveSharedMutex::lock() +{ + if (_uniqueOwner == std::this_thread::get_id()) + { + // already locked + _uniqueCount++; + } + else + { + _mutex.lock(); + _uniqueOwner = std::this_thread::get_id(); + assert(_uniqueCount == 0); + _uniqueCount = 1; + } +} + +void +RecursiveSharedMutex::unlock() +{ + assert(_uniqueCount > 0); + + if (--_uniqueCount == 0) + { + _uniqueOwner = {}; + _mutex.unlock(); + } +} + +void +RecursiveSharedMutex::lock_shared() +{ + if (_uniqueOwner == std::this_thread::get_id()) + { + // alone here, no need to lock + _sharedCounts[std::this_thread::get_id()]++; + + return; + } + + bool needLock {}; + + { + std::scoped_lock lock {_sharedCountMutex}; + + auto& sharedCount {_sharedCounts[std::this_thread::get_id()]}; + if (sharedCount == 0) + needLock = true; + else + ++sharedCount; + } + + if (needLock) + { + _mutex.lock_shared(); + + assert(_uniqueOwner == std::thread::id {}); + + std::scoped_lock lock {_sharedCountMutex}; + _sharedCounts[std::this_thread::get_id()]++; + } +} + +void +RecursiveSharedMutex::unlock_shared() +{ + if (_uniqueOwner == std::this_thread::get_id()) + { + // alone here, no need to lock + auto& sharedCount {_sharedCounts[std::this_thread::get_id()]}; + assert(sharedCount > 0); + --sharedCount; + + return; + } + + bool needUnlock {}; + + { + std::scoped_lock lock {_sharedCountMutex}; + + auto& sharedCount {_sharedCounts[std::this_thread::get_id()]}; + assert(sharedCount > 0); + needUnlock = (--sharedCount == 0); + } + + if (needUnlock) + _mutex.unlock_shared(); +} diff --git a/src/libs/utils/include/utils/IConfig.hpp b/src/libs/utils/include/utils/IConfig.hpp index 2612f6f5..409d3702 100644 --- a/src/libs/utils/include/utils/IConfig.hpp +++ b/src/libs/utils/include/utils/IConfig.hpp @@ -18,9 +18,8 @@ */ #pragma once +#include #include -#include -#include // Used to get config values from configuration files class IConfig @@ -30,11 +29,11 @@ class IConfig virtual ~IConfig() = default; // Default values are returned in case of setting not found - virtual std::string getString(const std::string& setting, const std::string& def = "", const std::unordered_set& allowedValues = {}) = 0; - virtual std::filesystem::path getPath(const std::string& setting, const std::filesystem::path& def = std::filesystem::path()) = 0; - virtual unsigned long getULong(const std::string& setting, unsigned long def = 0) = 0; - virtual long getLong(const std::string& setting, long def = 0) = 0; - virtual bool getBool(const std::string& setting, bool def = false) = 0; + virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0; + virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0; + virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0; + virtual long getLong(std::string_view setting, long def = 0) = 0; + virtual bool getBool(std::string_view setting, bool def = false) = 0; }; diff --git a/src/libs/utils/include/utils/RecursiveSharedMutex.hpp b/src/libs/utils/include/utils/RecursiveSharedMutex.hpp new file mode 100644 index 00000000..84f05453 --- /dev/null +++ b/src/libs/utils/include/utils/RecursiveSharedMutex.hpp @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +// API compatible with shared_mutex +class RecursiveSharedMutex +{ + public: + void lock(); + void unlock(); + + void lock_shared(); + void unlock_shared(); + + private: + std::shared_mutex _mutex; + std::thread::id _uniqueOwner; + std::size_t _uniqueCount{}; + + std::mutex _sharedCountMutex; + std::unordered_map _sharedCounts; +}; + diff --git a/src/libs/utils/include/utils/Service.hpp b/src/libs/utils/include/utils/Service.hpp index 4c2cf3e7..4aebe5e6 100644 --- a/src/libs/utils/include/utils/Service.hpp +++ b/src/libs/utils/include/utils/Service.hpp @@ -26,6 +26,7 @@ template class Service { public: + Service() = default; Service(std::unique_ptr service) { assign(std::move(service)); @@ -52,14 +53,17 @@ class Service } static Class* get() { return _service.get(); } + static bool exists() { return _service.get(); } - private: - static Class& assign(std::unique_ptr service) + template + static Class& assign(std::unique_ptr service) { assert(!_service); _service = std::move(service); return *get(); } + + private: static void clear() { _service.reset(); } static inline std::unique_ptr _service; diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 800c9e8d..a058abfb 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -14,9 +14,11 @@ add_executable(lms ui/admin/InitWizardView.cpp ui/admin/UserView.cpp ui/admin/UsersView.cpp - ui/common/AuthModeModel.cpp + ui/common/DirectoryValidator.cpp ui/common/LoadingIndicator.cpp - ui/common/Validators.cpp + ui/common/LoginNameValidator.cpp + ui/common/MandatoryValidator.cpp + ui/common/PasswordValidator.cpp ui/explore/ArtistListHelpers.cpp ui/explore/ArtistView.cpp ui/explore/ArtistsView.cpp diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 3a1cda38..ac68e93d 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -26,6 +26,7 @@ #include "auth/IAuthTokenService.hpp" #include "auth/IPasswordService.hpp" +#include "auth/IEnvService.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Db.hpp" #include "database/Session.hpp" @@ -36,6 +37,7 @@ #include "utils/IChildProcessManager.hpp" #include "utils/IConfig.hpp" #include "utils/Service.hpp" +#include "utils/String.hpp" #include "utils/WtLogger.hpp" static @@ -52,24 +54,24 @@ generateWtConfig(std::string execPath) args.push_back(execPath); args.push_back("--config=" + wtConfigPath.string()); - args.push_back("--docroot=" + Service::get()->getString("docroot")); - args.push_back("--approot=" + Service::get()->getString("approot")); - args.push_back("--deploy-path=" + Service::get()->getString("deploy-path", "/")); + args.push_back("--docroot=" + std::string {Service::get()->getString("docroot")}); + args.push_back("--approot=" + std::string {Service::get()->getString("approot")}); + args.push_back("--deploy-path=" + std::string {Service::get()->getString("deploy-path", "/")}); if (!wtResourcesPath.empty()) args.push_back("--resources-dir=" + wtResourcesPath.string()); if (Service::get()->getBool("tls-enable", false)) { args.push_back("--https-port=" + std::to_string( Service::get()->getULong("listen-port", 5082))); - args.push_back("--https-address=" + Service::get()->getString("listen-addr", "0.0.0.0")); - args.push_back("--ssl-certificate=" + Service::get()->getString("tls-cert")); - args.push_back("--ssl-private-key=" + Service::get()->getString("tls-key")); - args.push_back("--ssl-tmp-dh=" + Service::get()->getString("tls-dh")); + args.push_back("--https-address=" + std::string {Service::get()->getString("listen-addr", "0.0.0.0")}); + args.push_back("--ssl-certificate=" + std::string {Service::get()->getString("tls-cert")}); + args.push_back("--ssl-private-key=" + std::string {Service::get()->getString("tls-key")}); + args.push_back("--ssl-tmp-dh=" + std::string {Service::get()->getString("tls-dh")}); } else { args.push_back("--http-port=" + std::to_string( Service::get()->getULong("listen-port", 5082))); - args.push_back("--http-address=" + Service::get()->getString("listen-addr", "0.0.0.0")); + args.push_back("--http-address=" + std::string {Service::get()->getString("listen-addr", "0.0.0.0")}); } if (!wtAccessLogFilePath.empty()) @@ -211,10 +213,26 @@ int main(int argc, char* argv[]) UserInterface::LmsApplicationGroupContainer appGroups; - // Service initialization order is important + // Service initialization order is important (reverse-order for deinit) Service childProcessManagerService {createChildProcessManager()}; - Service authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))}; - Service passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))}; + + Service authTokenService; + Service authPasswordService; + Service authEnvService; + + const std::string authenticationBackend {StringUtils::stringToLower(config->getString("authentication-backend", "internal"))}; + if (authenticationBackend == "internal" || authenticationBackend == "pam") + { + authTokenService.assign(Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))); + authPasswordService.assign(Auth::createPasswordService(authenticationBackend, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get())); + } + else if (authenticationBackend == "http-headers") + { + authEnvService.assign(Auth::createEnvService(authenticationBackend)); + } + else + throw LmsException {"Bad value '" + authenticationBackend + "' for 'authentication-backend'"}; + Service coverArtService {CoverArt::createGrabber(argv[0], server.appRoot() + "/images/unknown-cover.jpg", config->getULong("cover-max-cache-size", 30) * 1000 * 1000, diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index c871d2a3..4e84eaa6 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -30,13 +30,17 @@ #include "auth/IAuthTokenService.hpp" #include "auth/IPasswordService.hpp" #include "database/Session.hpp" +#include "database/User.hpp" #include "utils/Logger.hpp" #include "utils/Service.hpp" -#include "common/Validators.hpp" +#include "common/LoginNameValidator.hpp" +#include "common/MandatoryValidator.hpp" +#include "common/PasswordValidator.hpp" #include "LmsApplication.hpp" -namespace UserInterface { +namespace UserInterface +{ static const std::string authCookieName {"LmsAuth"}; @@ -65,12 +69,12 @@ processAuthToken(const Wt::WEnvironment& env) const auto res {Service<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)}; switch (res.state) { - case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::NotFound: + case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Denied: case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled: LmsApp->setCookie(authCookieName, std::string {}, 0, "", "", env.urlScheme() == "https"); return std::nullopt; - case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Found: + case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Granted: createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry); break; } @@ -93,7 +97,7 @@ class AuthModel : public Wt::WFormModel addField(PasswordField); addField(RememberMeField); - setValidator(LoginNameField, createNameValidator()); + setValidator(LoginNameField, createLoginNameValidator()); setValidator(PasswordField, createMandatoryValidator()); } @@ -125,18 +129,20 @@ class AuthModel : public Wt::WFormModel if (field == PasswordField) { - switch (Service<::Auth::IPasswordService>::get()->checkUserPassword( + const auto checkResult {Service<::Auth::IPasswordService>::get()->checkUserPassword( LmsApp->getDbSession(), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), valueText(LoginNameField).toUTF8(), - valueText(PasswordField).toUTF8())) + valueText(PasswordField).toUTF8())}; + switch (checkResult.state) { - case ::Auth::IPasswordService::PasswordCheckResult::Match: + case ::Auth::IPasswordService::CheckResult::State::Granted: + _userId = *checkResult.userId; break; - case ::Auth::IPasswordService::PasswordCheckResult::Mismatch: + case ::Auth::IPasswordService::CheckResult::State::Denied: error = Wt::WString::tr("Lms.password-bad-login-combination"); break; - case ::Auth::IPasswordService::PasswordCheckResult::Throttled: + case ::Auth::IPasswordService::CheckResult::State::Throttled: error = Wt::WString::tr("Lms.password-client-throttled"); break; } diff --git a/src/lms/ui/Auth.hpp b/src/lms/ui/Auth.hpp index 3860a512..421271dc 100644 --- a/src/lms/ui/Auth.hpp +++ b/src/lms/ui/Auth.hpp @@ -36,7 +36,4 @@ class Auth : public Wt::WTemplateFormView Wt::Signal userLoggedIn; }; - } // namespace UserInterface - - diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 2c81e9c7..861b3501 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -28,6 +28,8 @@ #include #include +#include "auth/IEnvService.hpp" +#include "auth/IPasswordService.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" @@ -56,7 +58,6 @@ #include "PlayQueue.hpp" #include "SettingsView.hpp" - namespace UserInterface { static constexpr const char* defaultPath {"/releases"}; @@ -82,16 +83,16 @@ LmsApplication::getDbSession() Wt::Dbo::ptr LmsApplication::getUser() { - if (!_userId) + if (!_authenticatedUser) return {}; - return Database::User::getById(getDbSession(), *_userId); + return Database::User::getById(getDbSession(), _authenticatedUser->userId); } bool LmsApplication::isUserAuthStrong() const { - return *_userAuthStrong; + return _authenticatedUser->strongAuth; } bool @@ -125,7 +126,27 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, _db {db}, _appGroups {appGroups} { + try + { + init(); + } + catch (LmsApplicationException& e) + { + LMS_LOG(UI, WARNING) << "Caught a LmsApplication exception: " << e.what(); + handleException(e); + } + catch (std::exception& e) + { + LMS_LOG(UI, ERROR) << "Caught exception: " << e.what(); + throw LmsException {"Internal error"}; // Do not put details here at it may appear on the user rendered html + } +} + +LmsApplication::~LmsApplication() = default; +void +LmsApplication::init() +{ useStyleSheet("resources/font-awesome/css/font-awesome.min.css"); // Add a resource bundle @@ -160,80 +181,82 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env, // Handle Media Scanner events and other session events enableUpdates(true); - // If here is no account in the database, launch the first connection wizard - bool firstConnection {}; - { - auto transaction {getDbSession().createSharedTransaction()}; - firstConnection = Database::User::getAll(getDbSession()).empty(); - } - - LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection; + if (Service<::Auth::IEnvService>::exists()) + processEnvAuth(); + else if (Service<::Auth::IPasswordService>::exists()) + processPasswordAuth(); + else + throw LmsException {"No auth service available!"}; +} - if (firstConnection) - { - setTheme(std::make_unique(Database::User::defaultUITheme)); - root()->addWidget(std::make_unique()); - return; - } +void +LmsApplication::processEnvAuth() +{ + const auto checkResult {Service<::Auth::IEnvService>::get()->processEnv(getDbSession(), wApp->environment())}; + if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) + throw DeploymentException {}; - const auto userId {processAuthToken(env)}; + _authenticatedUser = {*checkResult.userId, false}; + onUserLoggedIn(); +} +void +LmsApplication::processPasswordAuth() +{ { - Database::User::UITheme theme {Database::User::defaultUITheme}; + std::optional userId {processAuthToken(environment())}; if (userId) { - auto transaction {getDbSession().createSharedTransaction()}; - const auto user {Database::User::getById(getDbSession(), *userId)}; - if (user) - theme = user->getUITheme(); + LMS_LOG(UI, DEBUG) << "User authenticated using Auth token!"; + _authenticatedUser = {*userId, false}; + onUserLoggedIn(); + return; } + } - setTheme(std::make_unique(theme)); + setTheme(); + + // If here is no account in the database, launch the first connection wizard + bool firstConnection {}; + { + auto transaction {getDbSession().createSharedTransaction()}; + firstConnection = Database::User::getCount(getDbSession()) == 0; } - if (userId) + LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection; + + if (firstConnection && Service<::Auth::IPasswordService>::get()->canSetPasswords()) { - try - { - handleUserLoggedIn(*userId, false); - } - catch (LmsApplicationException& e) - { - LMS_LOG(UI, WARNING) << "Caught a LmsApplication exception: " << e.what(); - handleException(e); - } - catch (std::exception& e) - { - LMS_LOG(UI, ERROR) << "Caught exception: " << e.what(); - throw LmsException {"Internal error"}; // Do not put details here at it may appear on the user rendered html - } + root()->addWidget(std::make_unique()); } else { Auth* auth {root()->addNew()}; auth->userLoggedIn.connect(this, [this](Database::IdType userId) { - { - auto transaction {getDbSession().createSharedTransaction()}; - const auto user {Database::User::getById(getDbSession(), userId)}; - if (user) - { - LmsTheme* lmsTheme {static_cast(LmsApp->theme().get())}; - lmsTheme->setTheme(user->getUITheme()); - } - } - - handleUserLoggedIn(userId, true); + _authenticatedUser = {userId, true}; + onUserLoggedIn(); }); } } -LmsApplication::~LmsApplication() = default; +void +LmsApplication::setTheme() +{ + Database::User::UITheme theme {Database::User::defaultUITheme}; + { + auto transaction {getDbSession().createSharedTransaction()}; + if (const auto user {getUser()}) + theme = user->getUITheme(); + } + + WApplication::setTheme(std::make_unique(theme)); +} void LmsApplication::finalize() { - if (_userId) + if (_authenticatedUser) { LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment()); @@ -403,28 +426,25 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin) LmsApplicationGroup& LmsApplication::getApplicationGroup() { - return _appGroups.get(*_userId); + return _appGroups.get(_authenticatedUser->userId); } void -LmsApplication::handleUserLoggedOut() +LmsApplication::logoutUser() { - LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out"; - { auto transaction {getDbSession().createUniqueTransaction()}; getUser().modify()->clearAuthTokens(); } + LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out"; goHomeAndQuit(); } void -LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth) +LmsApplication::onUserLoggedIn() { - _userId = userId; - _userAuthStrong = strongAuth; - + setTheme(); root()->clear(); const LmsApplicationInfo info {LmsApplicationInfo::fromEnvironment(environment())}; @@ -466,9 +486,9 @@ LmsApplication::createHome() main->bindNew("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings")); { - auto* logout {main->bindNew("logout")}; + Wt::WAnchor* logout {main->bindNew("logout")}; logout->setText(Wt::WString::tr("Lms.logout")); - logout->clicked().connect(this, &LmsApplication::handleUserLoggedOut); + logout->clicked().connect(this, &LmsApplication::logoutUser); } Wt::WLineEdit* searchEdit {main->bindNew("search")}; @@ -483,10 +503,10 @@ LmsApplication::createHome() // Contents // Order is important in mainStack, see IdxRoot! - Wt::WStackedWidget* mainStack = main->bindNew("contents"); + Wt::WStackedWidget* mainStack {main->bindNew("contents")}; mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;"); - Explore* explore = mainStack->addNew(filters); + Explore* explore {mainStack->addNew(filters)}; _playQueue = mainStack->addNew(); mainStack->addNew(); diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 9718c534..453007a2 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -27,11 +27,6 @@ #include "LmsApplicationGroup.hpp" -namespace Wt -{ - class WPopupMenu; -} - namespace Database { class Artist; @@ -41,10 +36,13 @@ namespace Database class Session; class User; } +namespace Wt +{ + class WPopupMenu; +} namespace UserInterface { -class Auth; class CoverResource; class LmsApplicationException; class MediaPlayer; @@ -61,12 +59,14 @@ struct Events class LmsApplication : public Wt::WApplication { public: + LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups); ~LmsApplication(); static std::unique_ptr create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups); static LmsApplication* instance(); + // Session application data std::shared_ptr getCoverResource() { return _coverResource; } Database::Session& getDbSession(); // always thread safe @@ -108,14 +108,18 @@ class LmsApplication : public Wt::WApplication private: + void init(); + void setTheme(); + void processEnvAuth(); + void processPasswordAuth(); void handleException(LmsApplicationException& e); void goHomeAndQuit(); LmsApplicationGroup& getApplicationGroup(); // Signal slots - void handleUserLoggedOut(); - void handleUserLoggedIn(Database::IdType userId, bool strongAuth); + void logoutUser(); + void onUserLoggedIn(); void notify(const Wt::WEvent& event) override; void finalize() override; @@ -127,8 +131,12 @@ class LmsApplication : public Wt::WApplication LmsApplicationGroupContainer& _appGroups; Events _events; Scanner::Events _scannerEvents; - std::optional _userId; - std::optional _userAuthStrong; + struct UserAuthInfo + { + Database::IdType userId; + bool strongAuth {}; + }; + std::optional _authenticatedUser; std::shared_ptr _coverResource; MediaPlayer* _mediaPlayer {}; PlayQueue* _playQueue {}; diff --git a/src/lms/ui/LmsApplicationException.hpp b/src/lms/ui/LmsApplicationException.hpp index 61ef2dbd..3acdde67 100644 --- a/src/lms/ui/LmsApplicationException.hpp +++ b/src/lms/ui/LmsApplicationException.hpp @@ -31,6 +31,12 @@ class LmsApplicationException : public LmsException LmsApplicationException(const Wt::WString& error) : LmsException {error.toUTF8()} {} }; +class DeploymentException : public LmsApplicationException +{ + public: + DeploymentException() : LmsApplicationException {Wt::WString::tr("Lms.Error.deployment-error")} {} +}; + class ArtistNotFoundException : public LmsApplicationException { public: diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 1d5531b4..90e8a326 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -29,7 +29,8 @@ #include #include -#include "common/Validators.hpp" +#include "common/PasswordValidator.hpp" +#include "common/MandatoryValidator.hpp" #include "common/ValueStringModel.hpp" #include "auth/IPasswordService.hpp" @@ -68,8 +69,9 @@ class SettingsModel : public Wt::WFormModel using TranscodeModeModel = ValueStringModel; using ReplayGainModeModel = ValueStringModel; - SettingsModel(bool withOldPassword) - : _withOldPassword {withOldPassword} + SettingsModel(::Auth::IPasswordService* authPasswordService, bool withOldPassword) + : _authPasswordService {authPasswordService} + , _withOldPassword {withOldPassword} { initializeModels(); @@ -84,11 +86,18 @@ class SettingsModel : public Wt::WFormModel addField(SubsonicTranscodeBitrateField); addField(SubsonicTranscodeFormatField); - if (_withOldPassword) - addField(PasswordOldField); + if (_authPasswordService) + { + if (_withOldPassword) + { + addField(PasswordOldField); + setValidator(PasswordOldField, createPasswordCheckValidator()); + } - addField(PasswordField); - addField(PasswordConfirmField); + addField(PasswordField); + setValidator(PasswordField, createPasswordStrengthValidator(LmsApp->getUserLoginName())); + addField(PasswordConfirmField); + } setValidator(TranscodeModeField, createMandatoryValidator()); setValidator(TranscodeBitrateField, createMandatoryValidator()); @@ -118,11 +127,6 @@ class SettingsModel : public Wt::WFormModel void saveData() { - User::PasswordHash passwordHash; - - if (!valueText(PasswordField).empty()) - passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8()); - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; User::pointer user {LmsApp->getUser()}; @@ -172,15 +176,15 @@ class SettingsModel : public Wt::WFormModel user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); } - if (!valueText(PasswordField).empty()) - { - user.modify()->setPasswordHash(passwordHash); - user.modify()->clearAuthTokens(); - } - auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))}; if (subsonicArtistListModeRow) user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); + + if (_authPasswordService && !valueText(PasswordField).empty()) + { + _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); + } + } void loadData() @@ -242,46 +246,17 @@ class SettingsModel : public Wt::WFormModel if (field == PasswordOldField) { - if (!valueText(PasswordOldField).empty()) - { - switch (Service<::Auth::IPasswordService>::get()->checkUserPassword( - LmsApp->getDbSession(), - boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), - LmsApp->getUserLoginName(), - valueText(PasswordOldField).toUTF8())) - { - case ::Auth::IPasswordService::PasswordCheckResult::Match: - break; - case ::Auth::IPasswordService::PasswordCheckResult::Mismatch: - error = Wt::WString::tr("Lms.Settings.password-bad"); - break; - case ::Auth::IPasswordService::PasswordCheckResult::Throttled: - error = Wt::WString::tr("Lms.password-client-throttled"); - break; - } - } + if (valueText(PasswordOldField).empty() && !valueText(PasswordField).empty()) + error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password"); else - { - if (!valueText(PasswordField).empty()) - error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password"); - else - return Wt::WFormModel::validateField(field); - } + return Wt::WFormModel::validateField(field); } else if (field == PasswordField) { - if (!valueText(PasswordField).empty()) - { - if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8())) - error = Wt::WString::tr("Lms.password-too-weak"); - } + if (!valueText(PasswordOldField).empty() && valueText(PasswordField).empty()) + error = Wt::WString::tr("Wt.WValidator.Invalid"); else - { - if (!valueText(PasswordOldField).empty()) - error = Wt::WString::tr("Wt.WValidator.Invalid"); - else - return Wt::WFormModel::validateField(field); - } + return Wt::WFormModel::validateField(field); } else if (field == PasswordConfirmField) { @@ -336,6 +311,7 @@ class SettingsModel : public Wt::WFormModel _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), User::SubsonicArtistListMode::TrackArtists); } + ::Auth::IPasswordService* _authPasswordService {}; bool _withOldPassword {}; std::shared_ptr _transcodeModeModel; @@ -374,7 +350,11 @@ SettingsView::refreshView() auto t {addNew(Wt::WString::tr("Lms.Settings.template"))}; - auto model {std::make_shared(!LmsApp->isUserAuthStrong())}; + auto* authPasswordService {Service<::Auth::IPasswordService>::get()}; + if (authPasswordService && !authPasswordService->canSetPasswords()) + authPasswordService = nullptr; + + auto model {std::make_shared(authPasswordService, !LmsApp->isUserAuthStrong())}; // Appearance { @@ -382,28 +362,33 @@ SettingsView::refreshView() t->setFormWidget(SettingsModel::DarkModeField, std::move(darkMode)); } - // Old password - if (!LmsApp->isUserAuthStrong()) + if (authPasswordService) { - t->setCondition("if-has-old-password", true); + t->setCondition("if-has-change-password", true); - auto oldPassword {std::make_unique()}; - oldPassword->setEchoMode(Wt::EchoMode::Password); - oldPassword->setAttributeValue("autocomplete", "current-password"); - t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword)); - } + // Old password + if (!LmsApp->isUserAuthStrong()) + { + t->setCondition("if-has-old-password", true); - // Password - auto password {std::make_unique()}; - password->setEchoMode(Wt::EchoMode::Password); - password->setAttributeValue("autocomplete", "new-password"); - t->setFormWidget(SettingsModel::PasswordField, std::move(password)); - - // Password confirm - auto passwordConfirm {std::make_unique()}; - passwordConfirm->setEchoMode(Wt::EchoMode::Password); - passwordConfirm->setAttributeValue("autocomplete", "new-password"); - t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); + auto oldPassword {std::make_unique()}; + oldPassword->setEchoMode(Wt::EchoMode::Password); + oldPassword->setAttributeValue("autocomplete", "current-password"); + t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword)); + } + + // Password + auto password {std::make_unique()}; + password->setEchoMode(Wt::EchoMode::Password); + password->setAttributeValue("autocomplete", "new-password"); + t->setFormWidget(SettingsModel::PasswordField, std::move(password)); + + // Password confirm + auto passwordConfirm {std::make_unique()}; + passwordConfirm->setEchoMode(Wt::EchoMode::Password); + passwordConfirm->setAttributeValue("autocomplete", "new-password"); + t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); + } // Audio { diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index 6c37dca7..dae20a93 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -34,7 +34,8 @@ #include "utils/Service.hpp" #include "utils/String.hpp" -#include "common/Validators.hpp" +#include "common/DirectoryValidator.hpp" +#include "common/MandatoryValidator.hpp" #include "common/ValueStringModel.hpp" #include "ScannerController.hpp" #include "LmsApplication.hpp" @@ -66,7 +67,7 @@ class DatabaseSettingsModel : public Wt::WFormModel addField(RecommendationEngineTypeField); addField(TagsField); - auto dirValidator {std::make_shared()}; + auto dirValidator {createDirectoryValidator()}; dirValidator->setMandatory(true); setValidator(MediaDirectoryField, dirValidator); diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index 39b5776f..be9561b3 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -31,8 +31,9 @@ #include "utils/Logger.hpp" #include "utils/Service.hpp" -#include "common/Validators.hpp" -#include "common/AuthModeModel.hpp" +#include "common/LoginNameValidator.hpp" +#include "common/MandatoryValidator.hpp" +#include "common/PasswordValidator.hpp" #include "LmsApplication.hpp" namespace UserInterface { @@ -42,118 +43,63 @@ class InitWizardModel : public Wt::WFormModel public: // Associate each field with a unique string literal. - static const Field AdminLoginField; - static const Field PasswordField; - static const Field PasswordConfirmField; - static inline const Field AuthModeField{"auth-mode"}; + static inline const Field AdminLoginField {"admin-login"}; + static inline const Field PasswordField {"password"}; + static inline const Field PasswordConfirmField {"password-confirm"}; InitWizardModel() : Wt::WFormModel() { addField(AdminLoginField); - addField(AuthModeField); addField(PasswordField); addField(PasswordConfirmField); - setValidator(AuthModeField, createMandatoryValidator()); - setValidator(AdminLoginField, createNameValidator()); - setValidator(PasswordField, createMandatoryValidator()); + setValidator(AdminLoginField, createLoginNameValidator()); + setValidator(PasswordField, createPasswordStrengthValidator([this] { return valueText(AdminLoginField).toUTF8(); })); + validator(PasswordField)->setMandatory(true); setValidator(PasswordConfirmField, createMandatoryValidator()); } - std::shared_ptr getAuthModeModel() const { return _authModeModel; } - void saveData() { - const Database::User::PasswordHash passwordHash {Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8())}; - auto transaction(LmsApp->getDbSession().createUniqueTransaction()); // Check if a user already exist // If it's the case, just do nothing if (!Database::User::getAll(LmsApp->getDbSession()).empty()) - throw LmsException("Admin user already created"); - - auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; - if (!authModeRow) - throw LmsException {"Bad authentication mode"}; - - const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)}; + throw LmsException {"Admin user already created"}; Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; user.modify()->setType(Database::User::Type::ADMIN); - user.modify()->setAuthMode(authMode); - if (authMode == Database::User::AuthMode::Internal) - user.modify()->setPasswordHash(passwordHash); - } - - void validatePassword(Wt::WString& error) const - { - auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; - if (!authModeRow) - throw LmsException {"Bad authentication mode"}; - - const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)}; - if (authMode != Database::User::AuthMode::Internal) - return; - - if (!valueText(PasswordField).empty()) - { - // Evaluate the strength of the password - if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8())) - error = Wt::WString::tr("Lms.password-too-weak"); - } - else - error = Wt::WString::tr("Lms.password-must-not-be-empty"); - } - - void validatePasswordConfirm(Wt::WString& error) const - { - auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; - if (!authModeRow) - throw LmsException {"Bad authentication mode"}; - - const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)}; - if (authMode != Database::User::AuthMode::Internal) - return; - - if (validation(PasswordField).state() == Wt::ValidationState::Valid) - { - if (valueText(PasswordField) != valueText(PasswordConfirmField)) - error = Wt::WString::tr("Lms.passwords-dont-match"); - } + Service<::Auth::IPasswordService>::get()->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); } bool validateField(Field field) { Wt::WString error; - if (field == PasswordField) + if (field == PasswordConfirmField) { - validatePassword(error); - } - else if (field == PasswordConfirmField) - { - validatePasswordConfirm(error); + if (validation(PasswordField).state() == Wt::ValidationState::Valid + && valueText(PasswordField) != valueText(PasswordConfirmField)) + { + error = Wt::WString::tr("Lms.passwords-dont-match"); + } + else + return Wt::WFormModel::validateField(field); } else { return Wt::WFormModel::validateField(field); } - setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error)); + setValidation(field, Wt::WValidator::Result {Wt::ValidationState::Invalid, error}); - return (validation(field).state() == Wt::ValidationState::Valid); + return false; } - - std::shared_ptr _authModeModel {createAuthModeModel()}; }; -const Wt::WFormModel::Field InitWizardModel::AdminLoginField = "admin-login"; -const Wt::WFormModel::Field InitWizardModel::PasswordField = "password"; -const Wt::WFormModel::Field InitWizardModel::PasswordConfirmField = "password-confirm"; - InitWizardView::InitWizardView() -: Wt::WTemplateFormView(Wt::WString::tr("Lms.Admin.InitWizard.template")) +: Wt::WTemplateFormView {Wt::WString::tr("Lms.Admin.InitWizard.template")} { auto model = std::make_shared(); @@ -164,20 +110,6 @@ InitWizardView::InitWizardView() setFormWidget(InitWizardModel::AdminLoginField, std::move(adminLogin)); } - // Auth mode - auto authMode = std::make_unique(); - authMode->setModel(model->getAuthModeModel()); - authMode->activated().connect([=](int row) - { - const Database::User::AuthMode authMode {model->getAuthModeModel()->getValue(row)}; - - model->setReadOnly(InitWizardModel::PasswordField, authMode != Database::User::AuthMode::Internal); - model->setReadOnly(InitWizardModel::PasswordConfirmField, authMode != Database::User::AuthMode::Internal); - updateModel(model.get()); - updateView(model.get()); - }); - setFormWidget(InitWizardModel::AuthModeField, std::move(authMode)); - // Password { auto passwordEdit = std::make_unique(); diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 4ec962cb..4d76dfd4 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -36,9 +36,8 @@ #include "utils/Service.hpp" #include "utils/String.hpp" -#include "common/AuthModeModel.hpp" -#include "common/Validators.hpp" -#include "common/ValueStringModel.hpp" +#include "common/LoginNameValidator.hpp" +#include "common/PasswordValidator.hpp" #include "LmsApplication.hpp" #include "LmsApplicationException.hpp" @@ -53,122 +52,62 @@ class UserModel : public Wt::WFormModel static inline const Field LoginField {"login"}; static inline const Field PasswordField {"password"}; static inline const Field DemoField {"demo"}; - static inline const Field AuthModeField{"auth-mode"}; - using AuthModeModel = ValueStringModel; - - UserModel(std::optional userId) + UserModel(std::optional userId, ::Auth::IPasswordService* authPasswordService) : _userId {userId} + , _authPasswordService {authPasswordService} { if (!_userId) { addField(LoginField); - setValidator(LoginField, createNameValidator()); + setValidator(LoginField, createLoginNameValidator()); } - addField(AuthModeField); - addField(PasswordField); + if (authPasswordService) + { + addField(PasswordField); + setValidator(PasswordField, createPasswordStrengthValidator([this] { return getLoginName(); })); + if (!userId) + validator(PasswordField)->setMandatory(true); + } addField(DemoField); - setValidator(AuthModeField, createMandatoryValidator()); - loadData(); } - std::shared_ptr getAuthModeModel() const { return _authModeModel; } - void saveData() { - std::optional passwordHash; - if (!valueText(PasswordField).empty()) - passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8()); - auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; if (_userId) { // Update user Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; + if (!user) + throw UserNotFoundException {*_userId}; - auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; - if (!authModeRow) - throw LmsException {"Bad authentication mode"}; - - const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)}; - user.modify()->setAuthMode(authMode); - if (authMode == Database::User::AuthMode::Internal && passwordHash) - { - user.modify()->setPasswordHash(*passwordHash); - user.modify()->clearAuthTokens(); - } + if (_authPasswordService && !valueText(PasswordField).empty()) + _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); } else { + // Check races with other endpoints (subsonic API...) + Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; + if (user) + throw UserNotAllowedException {}; + // Create user - Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8())}; + user = Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8()); if (Wt::asNumber(value(DemoField))) user.modify()->setType(Database::User::Type::DEMO); - auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; - if (!authModeRow) - throw LmsException {"Bad authentication mode"}; - - const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)}; - user.modify()->setAuthMode(authMode); - if (authMode == Database::User::AuthMode::Internal) - user.modify()->setPasswordHash(*passwordHash); + if (_authPasswordService) + _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8()); } } private: - - void validatePassword(Wt::WString& error) const - { - auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; - if (!authModeRow) - throw LmsException {"Bad authentication mode"}; - - const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)}; - if (authMode != Database::User::AuthMode::Internal) - return; - - if (!valueText(PasswordField).empty()) - { - if (Wt::asNumber(value(DemoField))) - { - // Demo account: password must be the same as the login name - if (valueText(PasswordField) != getLoginName()) - error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid"); - } - else - { - // Evaluate the strength of the password for non demo accounts - if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8())) - error = Wt::WString::tr("Lms.password-too-weak"); - } - } - else - { - auto transaction {LmsApp->getDbSession().createSharedTransaction()}; - - bool needPassword {true}; - - // Allow an empty password if and only if the user previously had one set - if (_userId) - { - const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; - if (!user) - throw UserNotFoundException {*_userId}; - - needPassword = user->getPasswordHash().hash.empty(); - } - - if (needPassword) - error = Wt::WString::tr("Lms.password-must-not-be-empty"); - } - } - void loadData() { if (!_userId) @@ -181,14 +120,6 @@ class UserModel : public Wt::WFormModel throw UserNotFoundException {*_userId}; else if (user == LmsApp->getUser()) throw UserNotAllowedException {}; - - auto authModeRow {_authModeModel->getRowFromValue(user->getAuthMode())}; - if (authModeRow) - { - setValue(AuthModeField, _authModeModel->getString(*authModeRow)); - if (_authModeModel->getValue(*authModeRow) != User::AuthMode::Internal) - setReadOnly(PasswordField, true); - } } std::string getLoginName() const @@ -204,6 +135,16 @@ class UserModel : public Wt::WFormModel return valueText(LoginField).toUTF8(); } + void validatePassword(Wt::WString& error) const + { + if (!valueText(PasswordField).empty() && Wt::asNumber(value(DemoField))) + { + // Demo account: password must be the same as the login name + if (valueText(PasswordField) != getLoginName()) + error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid"); + } + } + bool validateField(Field field) { Wt::WString error; @@ -237,7 +178,7 @@ class UserModel : public Wt::WFormModel } std::optional _userId; - std::shared_ptr _authModeModel {createAuthModeModel()}; + ::Auth::IPasswordService* _authPasswordService {}; }; UserView::UserView() @@ -262,7 +203,11 @@ UserView::refreshView() Wt::WTemplateFormView* t {addNew(Wt::WString::tr("Lms.Admin.User.template"))}; - auto model {std::make_shared(userId)}; + auto* authPasswordService {Service<::Auth::IPasswordService>::get()}; + if (authPasswordService && !authPasswordService->canSetPasswords()) + authPasswordService = nullptr; + + auto model {std::make_shared(userId, authPasswordService)}; if (userId) { @@ -284,31 +229,23 @@ UserView::refreshView() t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create")); } - // Auth mode - auto authMode = std::make_unique(); - authMode->setModel(model->getAuthModeModel()); - authMode->activated().connect([=](int row) + if (authPasswordService) { - const User::AuthMode authMode {model->getAuthModeModel()->getValue(row)}; + t->setCondition("if-has-password", true); - model->setReadOnly(UserModel::PasswordField, authMode != User::AuthMode::Internal); - t->updateModel(model.get()); - t->updateView(model.get()); - }); - t->setFormWidget(UserModel::AuthModeField, std::move(authMode)); - - // Password - auto passwordEdit = std::make_unique(); - passwordEdit->setEchoMode(Wt::EchoMode::Password); - passwordEdit->setAttributeValue("autocomplete", "off"); - t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit)); + // Password + auto passwordEdit = std::make_unique(); + passwordEdit->setEchoMode(Wt::EchoMode::Password); + passwordEdit->setAttributeValue("autocomplete", "off"); + t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit)); + } // Demo account t->setFormWidget(UserModel::DemoField, std::make_unique()); if (!userId && Service::get()->getBool("demo", false)) t->setCondition("if-demo", true); - Wt::WPushButton* saveBtn = t->bindNew("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create")); + Wt::WPushButton* saveBtn {t->bindNew("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create"))}; saveBtn->clicked().connect([=]() { t->updateModel(model.get()); diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index 853023c8..a4fc63ca 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -23,9 +23,11 @@ #include #include +#include "auth/IPasswordService.hpp" #include "database/User.hpp" #include "database/Session.hpp" #include "utils/Logger.hpp" +#include "utils/Service.hpp" #include "LmsApplication.hpp" @@ -38,11 +40,16 @@ UsersView::UsersView() _container = bindNew("users"); - Wt::WPushButton* addBtn = bindNew("add-btn", Wt::WString::tr("Lms.Admin.Users.add")); - addBtn->clicked().connect([]() + if (Service<::Auth::IPasswordService>::get() && Service<::Auth::IPasswordService>::get()->canSetPasswords()) { - LmsApp->setInternalPath("/admin/user", true); - }); + setCondition("if-can-create-user", true); + + Wt::WPushButton* addBtn = bindNew("add-btn", Wt::WString::tr("Lms.Admin.Users.add")); + addBtn->clicked().connect([]() + { + LmsApp->setInternalPath("/admin/user", true); + }); + } wApp->internalPathChanged().connect(this, [this]() { diff --git a/src/lms/ui/common/DirectoryValidator.cpp b/src/lms/ui/common/DirectoryValidator.cpp new file mode 100644 index 00000000..b55fe38e --- /dev/null +++ b/src/lms/ui/common/DirectoryValidator.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "DirectoryValidator.hpp" + +#include + +namespace UserInterface +{ + class DirectoryValidator : public Wt::WValidator + { + public: + Wt::WValidator::Result validate(const Wt::WString& input) const override; + }; + + Wt::WValidator::Result + DirectoryValidator::validate(const Wt::WString& input) const + { + if (input.empty()) + return Wt::WValidator::validate(input); + + const std::filesystem::path p {input.toUTF8()}; + std::error_code ec; + + // TODO check rights + bool res = std::filesystem::is_directory(p, ec); + if (ec) + return Wt::WValidator::Result(Wt::ValidationState::Invalid, ec.message()); // TODO translate common errors + else if (res) + return Wt::WValidator::Result(Wt::ValidationState::Valid); + else + return Wt::WValidator::Result(Wt::ValidationState::Invalid, Wt::WString::tr("Lms.not-a-directory")); + } + + std::shared_ptr + createDirectoryValidator() + { + return std::make_unique(); + } +} // namespace UserInterface diff --git a/src/libs/auth/impl/pam/PAM.hpp b/src/lms/ui/common/DirectoryValidator.hpp similarity index 76% rename from src/libs/auth/impl/pam/PAM.hpp rename to src/lms/ui/common/DirectoryValidator.hpp index dd0e9732..08cc26aa 100644 --- a/src/libs/auth/impl/pam/PAM.hpp +++ b/src/lms/ui/common/DirectoryValidator.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Emeric Poupon + * Copyright (C) 2021 Emeric Poupon * * This file is part of LMS. * @@ -19,14 +19,10 @@ #pragma once -#ifdef LMS_SUPPORT_PAM +#include -#include - -namespace Auth::PAM +namespace UserInterface { - bool checkUserPassword(const std::string& loginName, const std::string& password); -} - -#endif // LMS_SUPPORT_PAM + std::shared_ptr createDirectoryValidator(); +} // namespace UserInterface diff --git a/src/lms/ui/common/Validators.hpp b/src/lms/ui/common/LoginNameValidator.cpp similarity index 67% rename from src/lms/ui/common/Validators.hpp rename to src/lms/ui/common/LoginNameValidator.cpp index 11f9d502..d1574d3f 100644 --- a/src/lms/ui/common/Validators.hpp +++ b/src/lms/ui/common/LoginNameValidator.cpp @@ -17,25 +17,20 @@ * along with LMS. If not, see . */ -#pragma once - -#include +#include "LoginNameValidator.hpp" +#include #include "database/User.hpp" -namespace UserInterface { - -std::shared_ptr createNameValidator(); -std::shared_ptr createMandatoryValidator(); - -class DirectoryValidator : public Wt::WValidator +namespace UserInterface { - public: - DirectoryValidator(); - - Wt::WValidator::Result validate(const Wt::WString& input) const override; - -}; - + std::shared_ptr + createLoginNameValidator() + { + auto v = std::make_unique(); + v->setMandatory(true); + v->setMinimumLength(::Database::User::MinNameLength); + v->setMaximumLength(::Database::User::MaxNameLength); + return v; + } } // namespace UserInterface - diff --git a/src/lms/ui/common/AuthModeModel.hpp b/src/lms/ui/common/LoginNameValidator.hpp similarity index 75% rename from src/lms/ui/common/AuthModeModel.hpp rename to src/lms/ui/common/LoginNameValidator.hpp index 171f1034..b67c92d5 100644 --- a/src/lms/ui/common/AuthModeModel.hpp +++ b/src/lms/ui/common/LoginNameValidator.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Emeric Poupon + * Copyright (C) 2013 Emeric Poupon * * This file is part of LMS. * @@ -19,13 +19,10 @@ #pragma once -#include "database/User.hpp" -#include "common/ValueStringModel.hpp" +#include namespace UserInterface { - using AuthModeModel = ValueStringModel; - - std::unique_ptr createAuthModeModel(); -} + std::shared_ptr createLoginNameValidator(); +} // namespace UserInterface diff --git a/src/lms/ui/common/MandatoryValidator.cpp b/src/lms/ui/common/MandatoryValidator.cpp new file mode 100644 index 00000000..ca60f369 --- /dev/null +++ b/src/lms/ui/common/MandatoryValidator.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "MandatoryValidator.hpp" + +namespace UserInterface +{ + std::shared_ptr + createMandatoryValidator() + { + auto v {std::make_shared()}; + v->setMandatory(true); + return v; + } +} // namespace UserInterface diff --git a/src/lms/ui/common/MandatoryValidator.hpp b/src/lms/ui/common/MandatoryValidator.hpp new file mode 100644 index 00000000..6fac9a04 --- /dev/null +++ b/src/lms/ui/common/MandatoryValidator.hpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#pragma once + +#include + +namespace UserInterface +{ + std::shared_ptr createMandatoryValidator(); +} // namespace UserInterface + diff --git a/src/lms/ui/common/PasswordValidator.cpp b/src/lms/ui/common/PasswordValidator.cpp new file mode 100644 index 00000000..881857e3 --- /dev/null +++ b/src/lms/ui/common/PasswordValidator.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include "PasswordValidator.hpp" + +#include "auth/IPasswordService.hpp" +#include "utils/Service.hpp" +#include "LmsApplication.hpp" + +namespace UserInterface +{ + class PasswordStrengthValidator : public Wt::WValidator + { + public: + PasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc) : _loginNameGetFunc {std::move(loginNameGetFunc)} {} + + Wt::WValidator::Result validate(const Wt::WString& input) const override; + + private: + LoginNameGetFunc _loginNameGetFunc; + }; + + Wt::WValidator::Result + PasswordStrengthValidator::validate(const Wt::WString& input) const + { + if (input.empty()) + return Wt::WValidator::validate(input); + + if (Service<::Auth::IPasswordService>::get()->isPasswordSecureEnough(_loginNameGetFunc(), input.toUTF8())) + return Wt::WValidator::Result {Wt::ValidationState::Valid}; + + return Wt::WValidator::Result {Wt::ValidationState::Invalid, Wt::WString::tr("Lms.password-too-weak")}; + } + + std::shared_ptr + createPasswordStrengthValidator(std::string_view loginName) + { + return std::make_shared([loginName = std::string {loginName}] { return loginName; }); + } + + std::shared_ptr createPasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc) + { + return std::make_shared(std::move(loginNameGetFunc)); + } + + class PasswordCheckValidator : public Wt::WValidator + { + public: + Wt::WValidator::Result validate(const Wt::WString& input) const override; + }; + + Wt::WValidator::Result + PasswordCheckValidator::validate(const Wt::WString& input) const + { + const auto checkResult {Service<::Auth::IPasswordService>::get()->checkUserPassword( + LmsApp->getDbSession(), + boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), + LmsApp->getUserLoginName(), + input.toUTF8())}; + switch (checkResult.state) + { + case ::Auth::IPasswordService::CheckResult::State::Granted: + return Wt::WValidator::Result {Wt::ValidationState::Valid}; + case ::Auth::IPasswordService::CheckResult::State::Denied: + return Wt::WValidator::Result {Wt::ValidationState::Invalid, Wt::WString::tr("Lms.Settings.password-bad")}; + case ::Auth::IPasswordService::CheckResult::State::Throttled: + return Wt::WValidator::Result {Wt::ValidationState::Invalid, Wt::WString::tr("Lms.password-client-throttled")}; + } + + throw LmsException {"InternalError"}; + } + + std::shared_ptr + createPasswordCheckValidator() + { + return std::make_shared(); + } + +} // namespace UserInterface + diff --git a/src/lms/ui/common/AuthModeModel.cpp b/src/lms/ui/common/PasswordValidator.hpp similarity index 51% rename from src/lms/ui/common/AuthModeModel.cpp rename to src/lms/ui/common/PasswordValidator.hpp index e96b6daa..481d0270 100644 --- a/src/lms/ui/common/AuthModeModel.cpp +++ b/src/lms/ui/common/PasswordValidator.hpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Emeric Poupon + * Copyright (C) 2021 Emeric Poupon * * This file is part of LMS. * @@ -17,26 +17,17 @@ * along with LMS. If not, see . */ -#include "AuthModeModel.hpp" +#pragma once -#include "auth/IPasswordService.hpp" -#include "utils/Service.hpp" +#include namespace UserInterface { + std::shared_ptr createPasswordStrengthValidator(std::string_view loginName); + using LoginNameGetFunc = std::function; + std::shared_ptr createPasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc); -std::unique_ptr -createAuthModeModel() -{ - auto model {std::make_unique()}; - - if (Service<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::Internal)) - model->add(Wt::WString::tr("Lms.Admin.User.auth-mode.internal"), Database::User::AuthMode::Internal); - if (Service<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::PAM)) - model->add(Wt::WString::tr("Lms.Admin.User.auth-mode.pam"), Database::User::AuthMode::PAM); - - return model; -} - -} + // Check current user password + std::shared_ptr createPasswordCheckValidator(); +} // namespace UserInterface diff --git a/src/lms/ui/common/Validators.cpp b/src/lms/ui/common/Validators.cpp deleted file mode 100644 index a1050b8c..00000000 --- a/src/lms/ui/common/Validators.cpp +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2013 Emeric Poupon - * - * This file is part of LMS. - * - * LMS is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LMS is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LMS. If not, see . - */ - -#include "Validators.hpp" - -#include - -#include - -namespace UserInterface { - -std::shared_ptr -createNameValidator() -{ - auto v = std::make_shared(); - v->setMandatory(true); - v->setMinimumLength(::Database::User::MinNameLength); - v->setMaximumLength(::Database::User::MaxNameLength); - return v; -} - -std::shared_ptr -createMandatoryValidator() -{ - auto v = std::make_shared(); - //sv->setMandatory(true); - return v; -} - -DirectoryValidator::DirectoryValidator() : Wt::WValidator() -{ -} - -Wt::WValidator::Result -DirectoryValidator::validate(const Wt::WString& input) const -{ - if (input.empty()) - return Wt::WValidator::validate(input); - - const std::filesystem::path p {input.toUTF8()}; - std::error_code ec; - - // TODO check rights - bool res = std::filesystem::is_directory(p, ec); - if (ec) - return Wt::WValidator::Result(Wt::ValidationState::Invalid, ec.message()); // TODO translate common errors - else if (res) - return Wt::WValidator::Result(Wt::ValidationState::Valid); - else - return Wt::WValidator::Result(Wt::ValidationState::Invalid, Wt::WString::tr("Lms.not-a-directory")); -} - - -} // namespace UserInterface diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt index d2d34662..ea5a1fe2 100644 --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(database) add_subdirectory(som) +add_subdirectory(utils) diff --git a/src/test/utils/CMakeLists.txt b/src/test/utils/CMakeLists.txt new file mode 100644 index 00000000..806f856f --- /dev/null +++ b/src/test/utils/CMakeLists.txt @@ -0,0 +1,12 @@ + +add_executable(test-utils + UtilsTest.cpp + ) + +target_link_libraries(test-utils PRIVATE + lmsutils + Threads::Threads + ) + +add_test(NAME utils COMMAND test-utils) + diff --git a/src/test/utils/UtilsTest.cpp b/src/test/utils/UtilsTest.cpp new file mode 100644 index 00000000..aad60881 --- /dev/null +++ b/src/test/utils/UtilsTest.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2019 Emeric Poupon + * + * This file is part of LMS. + * + * LMS is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * LMS is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with LMS. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "utils/RecursiveSharedMutex.hpp" + +void +testSharedMutex() +{ + { + RecursiveSharedMutex mutex; + + { + std::unique_lock lock {mutex}; + } + + { + std::shared_lock lock {mutex}; + } + + { + std::unique_lock lock1 {mutex}; + std::unique_lock lock2 {mutex}; + } + + { + std::shared_lock lock1 {mutex}; + std::shared_lock lock2 {mutex}; + } + + { + std::unique_lock lock1 {mutex}; + std::shared_lock lock2 {mutex}; + } + } + + { + constexpr std::size_t nbThreads {10}; + std::vector threads; + + RecursiveSharedMutex mutex; + std::atomic nbUnique {}; + std::atomic nbShared {}; + for (std::size_t i {}; i < nbThreads; ++i) + { + threads.emplace_back([&] + { + { + std::unique_lock lock {mutex}; + + std::shared_lock lock2 {mutex}; + + assert(nbUnique == 0); + assert(nbShared == 0); + nbUnique++; + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + assert(nbUnique == 1); + assert(nbShared == 0); + nbUnique--; + } + + { + std::shared_lock lock {mutex}; + std::shared_lock lock2 {mutex}; + + assert(nbUnique == 0); + nbShared++; + + std::this_thread::sleep_for(std::chrono::milliseconds(15)); + + assert(nbShared > 0); + assert(nbShared <= nbThreads); + assert(nbUnique == 0); + nbShared--; + } + }); + } + + for (std::thread& t : threads) + t.join(); + } +} + + +int main() +{ + try + { + testSharedMutex(); + } + catch (std::exception& e) + { + std::cerr << "Caught exception: " << e.what(); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} From 1b49e8f6a65494895ce0976e6d1a2576b7c56142 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 5 Mar 2021 13:23:47 +0100 Subject: [PATCH 4/5] Properly handled http headers for subsonic API. ref #119 --- INSTALL.md | 4 +- approot/messages.xml | 1 - approot/messages_fr.xml | 1 - conf/lms.conf | 2 +- .../http-headers/HttpHeadersEnvService.cpp | 18 +- .../http-headers/HttpHeadersEnvService.hpp | 1 + src/libs/auth/include/auth/IEnvService.hpp | 6 + src/libs/subsonic/impl/RequestContext.hpp | 6 +- src/libs/subsonic/impl/Stream.cpp | 2 +- src/libs/subsonic/impl/SubsonicResource.cpp | 157 +++++++++++------- src/lms/ui/LmsApplication.cpp | 42 ++--- src/lms/ui/LmsApplication.hpp | 3 +- src/lms/ui/LmsApplicationException.hpp | 6 - 13 files changed, 148 insertions(+), 101 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 5d923e06..7ff70afc 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -132,8 +132,8 @@ If a setting is not present in the configuration file, a hardcoded default value You can define which authentication backend to be used thanks to the `authentication-backend` option: * `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users. -* `PAM`: the authentication request is forwarded to PAM (see the [default configuration file](conf/pam/lms)). -* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-user-field` option. +* `PAM`: the user/password authentication request is forwarded to PAM (see the [default configuration file](conf/pam/lms)). +* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-login-field` option. __Note__: the first created user is the admin user diff --git a/approot/messages.xml b/approot/messages.xml index 28dff4ae..ee2a721c 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -28,7 +28,6 @@ Artist not found Error occured! -Deployment error Go home Release not found You are not allowed to perform this operation diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 641029b3..4e4c9af7 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -28,7 +28,6 @@ Cet artiste n'existe pas Une erreur est survenue! -Erreur de déploiement Retour à l'accueil Cet album n'existe pas Vous n'avez pas les droits pour effectuer cette opération diff --git a/conf/lms.conf b/conf/lms.conf index fc38a121..e4c00109 100644 --- a/conf/lms.conf +++ b/conf/lms.conf @@ -40,7 +40,7 @@ acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/"; # Authentication # Available backends: "internal", "PAM", "http-headers" authentication-backend = "internal"; -http-headers-user-field = "X-Forwarded-User"; +http-headers-login-field = "X-Forwarded-User"; # Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used) login-throttler-max-entries = 10000; diff --git a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp index 4d24085a..93f0b584 100644 --- a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp +++ b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.cpp @@ -29,7 +29,7 @@ namespace Auth { HttpHeadersEnvService::HttpHeadersEnvService() - : _fieldName {Service::get()->getString("http-headers-field-name", "X-Forwarded-User")} + : _fieldName {Service::get()->getString("http-headers-login-field", "X-Forwarded-User")} { LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'"; } @@ -37,7 +37,21 @@ namespace Auth HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processEnv(Database::Session& session, const Wt::WEnvironment& env) { - const std::string loginName { env.headerValue(_fieldName)}; + const std::string loginName {env.headerValue(_fieldName)}; + if (loginName.empty()) + return {CheckResult::State::Denied}; + + LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header"; + + const Database::IdType userId {getOrCreateUser(session, loginName)}; + onUserAuthenticated(session, userId); + return {CheckResult::State::Granted, userId}; + } + + HttpHeadersEnvService::CheckResult + HttpHeadersEnvService::processRequest(Database::Session& session, const Wt::Http::Request& request) + { + const std::string loginName {request.headerValue(_fieldName)}; if (loginName.empty()) return {CheckResult::State::Denied}; diff --git a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp index 2f74d0de..1295f182 100644 --- a/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp +++ b/src/libs/auth/impl/http-headers/HttpHeadersEnvService.hpp @@ -31,6 +31,7 @@ namespace Auth private: CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) override; + CheckResult processRequest(Database::Session& session, const Wt::Http::Request& request) override; std::string _fieldName; }; diff --git a/src/libs/auth/include/auth/IEnvService.hpp b/src/libs/auth/include/auth/IEnvService.hpp index 336432e3..02a6d4e2 100644 --- a/src/libs/auth/include/auth/IEnvService.hpp +++ b/src/libs/auth/include/auth/IEnvService.hpp @@ -34,6 +34,11 @@ namespace Wt class WEnvironment; } +namespace Wt::Http +{ + class Request; +} + namespace Auth { class IEnvService @@ -56,6 +61,7 @@ namespace Auth }; virtual CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) = 0; + virtual CheckResult processRequest(Database::Session& session, const Wt::Http::Request& request) = 0; }; std::unique_ptr createEnvService(std::string_view backendName); diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index cd41100b..6d599513 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -22,8 +22,8 @@ #include #include -#include +#include "database/Types.hpp" namespace Database { @@ -32,14 +32,12 @@ namespace Database namespace API::Subsonic { - struct RequestContext { const Wt::Http::ParameterMap& parameters; Database::Session& dbSession; - std::string userName; + Database::IdType userId; std::string clientName; }; - } diff --git a/src/libs/subsonic/impl/Stream.cpp b/src/libs/subsonic/impl/Stream.cpp index 625224e5..29085c83 100644 --- a/src/libs/subsonic/impl/Stream.cpp +++ b/src/libs/subsonic/impl/Stream.cpp @@ -81,7 +81,7 @@ getStreamParameters(RequestContext& context) } { - const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + const User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 6c922a21..70ba40ba 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -26,6 +26,7 @@ #include #include "auth/IPasswordService.hpp" +#include "auth/IEnvService.hpp" #include "cover/ICoverArtGrabber.hpp" #include "database/Artist.hpp" #include "database/Cluster.hpp" @@ -209,14 +210,33 @@ static void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username) { - if (username != context.userName) + User::pointer currentUser {User::getById(context.dbSession, context.userId)}; + if (!currentUser) + throw RequestedDataNotFoundError {}; + + if (currentUser->getLoginName() != username && !currentUser->isAdmin()) + throw UserNotAuthorizedError {}; +} + +static +void +checkUserIsAdmin(RequestContext& context) +{ + LMS_LOG(API_SUBSONIC, DEBUG) << "Check user is admin"; + + auto transaction {context.dbSession.createSharedTransaction()}; + + User::pointer currentUser {User::getById(context.dbSession, context.userId)}; + if (!currentUser) { - User::pointer currentUser {User::getByLoginName(context.dbSession, context.userName)}; - if (!currentUser) - throw RequestedDataNotFoundError {}; + LMS_LOG(API_SUBSONIC, DEBUG) << "NOT FOUND"; + throw RequestedDataNotFoundError {}; + } - if (!currentUser->isAdmin()) - throw UserNotAuthorizedError {}; + if (!currentUser->isAdmin()) + { + LMS_LOG(API_SUBSONIC, DEBUG) << "NOT ADMIN"; + throw UserNotAuthorizedError {}; } } @@ -327,7 +347,6 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("coverArt", IdToString({Id::Type::Track, track.id()})); const std::vector& artists {track->getArtists({TrackArtistLinkType::Artist})}; - LMS_LOG(API_SUBSONIC, DEBUG) << "Artists count = " << artists.size(); if (!artists.empty()) { trackResponse.setAttribute("artist", getArtistNames(artists)); @@ -573,7 +592,7 @@ handleCreatePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -629,12 +648,12 @@ handleCreateUserRequest(RequestContext& context) } auto removeCreatedUser {[&]() - { - auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getById(context.dbSession, userId)}; - if (user) - user.remove(); - }}; + { + auto transaction {context.dbSession.createUniqueTransaction()}; + User::pointer user {User::getById(context.dbSession, userId)}; + if (user) + user.remove(); + }}; try { @@ -664,7 +683,7 @@ handleDeletePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -687,16 +706,16 @@ handleDeleteUserRequest(RequestContext& context) { std::string username {getMandatoryParameterAs(context.parameters, "username")}; - // cannot delete ourself - if (username == context.userName) - throw UserNotAuthorizedError {}; - auto transaction {context.dbSession.createUniqueTransaction()}; User::pointer user {User::getByLoginName(context.dbSession, username)}; if (!user) throw RequestedDataNotFoundError {}; + // cannot delete ourself + if (user.id() == context.userId) + throw UserNotAuthorizedError {}; + user.remove(); return Response::createOkResponse(context); @@ -726,7 +745,7 @@ handleGetRandomSongsRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -758,7 +777,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -860,7 +879,7 @@ handleGetAlbumRequest(RequestContext& context) if (!release) throw RequestedDataNotFoundError {}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -892,7 +911,7 @@ handleGetArtistRequest(RequestContext& context) if (!artist) throw RequestedDataNotFoundError {}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -943,7 +962,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -987,7 +1006,7 @@ handleGetArtistsRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1031,7 +1050,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1137,7 +1156,7 @@ handleGetIndexesRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1190,7 +1209,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) if (!artist) throw RequestedDataNotFoundError {}; - const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + const User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1239,7 +1258,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1313,7 +1332,7 @@ handleGetPlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1339,7 +1358,7 @@ handleGetPlaylistsRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1376,7 +1395,7 @@ handleGetSongsByGenreRequest(RequestContext& context) if (!cluster) throw RequestedDataNotFoundError {}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1446,7 +1465,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1528,7 +1547,7 @@ handleStarRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1584,7 +1603,7 @@ handleUnstarRequest(RequestContext& context) auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw RequestedDataNotFoundError {}; @@ -1631,7 +1650,7 @@ handleScrobble(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw RequestedDataNotFoundError {}; @@ -1707,7 +1726,7 @@ handleUpdatePlaylistRequest(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1756,7 +1775,7 @@ handleGetBookmarks(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1790,7 +1809,7 @@ handleCreateBookmark(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + const User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1821,7 +1840,7 @@ handleDeleteBookmark(RequestContext& context) auto transaction {context.dbSession.createUniqueTransaction()}; - const User::pointer user {User::getByLoginName(context.dbSession, context.userName)}; + const User::pointer user {User::getById(context.dbSession, context.userId)}; if (!user) throw UserNotAuthorizedError {}; @@ -1995,6 +2014,39 @@ static std::unordered_map mediaRetrieval {"getCoverArt", handleGetCoverArt}, }; +static +Database::IdType +authenticateUser(const Wt::Http::Request &request, const ClientInfo& clientInfo, Session& dbSession) +{ + if (auto *authEnvService {Service<::Auth::IEnvService>::get()}) + { + const auto checkResult {authEnvService->processRequest(dbSession, request)}; + if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) + throw UserNotAuthorizedError {}; + + return *checkResult.userId; + } + else if (auto *authPasswordService {Service<::Auth::IPasswordService>::get()}) + { + const auto checkResult {authPasswordService->checkUserPassword(dbSession, + boost::asio::ip::address::from_string(request.clientAddress()), + clientInfo.user, clientInfo.password)}; + + switch (checkResult.state) + { + case Auth::IPasswordService::CheckResult::State::Granted: + return *checkResult.userId; + break; + case Auth::IPasswordService::CheckResult::State::Denied: + throw WrongUsernameOrPasswordError {}; + case Auth::IPasswordService::CheckResult::State::Throttled: + throw LoginThrottledGenericError {}; + } + } + + throw InternalErrorGenericError {"No service avalaible to authenticate user"}; +} + void SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) { @@ -2024,21 +2076,8 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp Session& dbSession {_db.getTLSSession()}; - const Auth::IPasswordService::CheckResult checkResult {Service::get()->checkUserPassword(dbSession, - boost::asio::ip::address::from_string(request.clientAddress()), - clientInfo.user, clientInfo.password)}; - - switch (checkResult.state) - { - case Auth::IPasswordService::CheckResult::State::Granted: - break; - case Auth::IPasswordService::CheckResult::State::Denied: - throw WrongUsernameOrPasswordError {}; - case Auth::IPasswordService::CheckResult::State::Throttled: - throw LoginThrottledGenericError {}; - } - - RequestContext requestContext {parameters, dbSession, clientInfo.user, clientInfo.name}; + const Database::IdType userId {authenticateUser(request, clientInfo, dbSession)}; + RequestContext requestContext {parameters, dbSession, userId, clientInfo.name}; auto itEntryPoint {requestEntryPoints.find(requestPath)}; if (itEntryPoint != requestEntryPoints.end()) @@ -2047,13 +2086,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp itEntryPoint->second.checkFunc(); if (itEntryPoint->second.mustBeAdmin) - { - auto transaction {dbSession.createSharedTransaction()}; - - User::pointer user {User::getByLoginName(dbSession, clientInfo.user)}; - if (!user || !user->isAdmin()) - throw UserNotAuthorizedError {}; - } + checkUserIsAdmin(requestContext); Response resp {(itEntryPoint->second.func)(requestContext)}; diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 861b3501..f86906ee 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -65,6 +65,19 @@ static constexpr const char* defaultPath {"/releases"}; std::unique_ptr LmsApplication::create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups) { + if (auto *authEnvService {Service<::Auth::IEnvService>::get()}) + { + const auto checkResult {authEnvService->processEnv(db.getTLSSession(), env)}; + if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) + { + LMS_LOG(UI, ERROR) << "Cannot authenticate user from environment!"; + // return a blank page + return std::make_unique(env); + } + + return std::make_unique(env, db, appGroups, checkResult.userId); + } + return std::make_unique(env, db, appGroups); } @@ -121,10 +134,12 @@ LmsApplication::getUserLoginName() LmsApplication::LmsApplication(const Wt::WEnvironment& env, Database::Db& db, - LmsApplicationGroupContainer& appGroups) -: Wt::WApplication {env}, - _db {db}, - _appGroups {appGroups} + LmsApplicationGroupContainer& appGroups, + std::optional userId) +: Wt::WApplication {env} +, _db {db} +, _appGroups {appGroups} +, _authenticatedUser {userId ? std::make_optional(UserAuthInfo {*userId, false}) : std::nullopt} { try { @@ -181,23 +196,12 @@ LmsApplication::init() // Handle Media Scanner events and other session events enableUpdates(true); - if (Service<::Auth::IEnvService>::exists()) - processEnvAuth(); + if (_authenticatedUser) + { + onUserLoggedIn(); + } else if (Service<::Auth::IPasswordService>::exists()) processPasswordAuth(); - else - throw LmsException {"No auth service available!"}; -} - -void -LmsApplication::processEnvAuth() -{ - const auto checkResult {Service<::Auth::IEnvService>::get()->processEnv(getDbSession(), wApp->environment())}; - if (checkResult.state != ::Auth::IEnvService::CheckResult::State::Granted) - throw DeploymentException {}; - - _authenticatedUser = {*checkResult.userId, false}; - onUserLoggedIn(); } void diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 453007a2..2fdd8d10 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -60,7 +60,7 @@ class LmsApplication : public Wt::WApplication { public: - LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups); + LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups, std::optional userId = std::nullopt); ~LmsApplication(); static std::unique_ptr create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups); @@ -110,7 +110,6 @@ class LmsApplication : public Wt::WApplication void init(); void setTheme(); - void processEnvAuth(); void processPasswordAuth(); void handleException(LmsApplicationException& e); void goHomeAndQuit(); diff --git a/src/lms/ui/LmsApplicationException.hpp b/src/lms/ui/LmsApplicationException.hpp index 3acdde67..61ef2dbd 100644 --- a/src/lms/ui/LmsApplicationException.hpp +++ b/src/lms/ui/LmsApplicationException.hpp @@ -31,12 +31,6 @@ class LmsApplicationException : public LmsException LmsApplicationException(const Wt::WString& error) : LmsException {error.toUTF8()} {} }; -class DeploymentException : public LmsApplicationException -{ - public: - DeploymentException() : LmsApplicationException {Wt::WString::tr("Lms.Error.deployment-error")} {} -}; - class ArtistNotFoundException : public LmsApplicationException { public: From 1762b08e42c20a916bf5fa278e6e4cbae7f2b3fe Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 5 Mar 2021 15:18:49 +0100 Subject: [PATCH 5/5] Review from code factor --- src/libs/auth/impl/internal/InternalPasswordService.hpp | 1 - src/libs/auth/impl/pam/PAMPasswordService.cpp | 2 -- src/libs/auth/impl/pam/PAMPasswordService.hpp | 1 - src/lms/ui/LmsApplication.hpp | 1 - src/lms/ui/SettingsView.cpp | 1 - 5 files changed, 6 deletions(-) diff --git a/src/libs/auth/impl/internal/InternalPasswordService.hpp b/src/libs/auth/impl/internal/InternalPasswordService.hpp index efcc9e06..6cef56fb 100644 --- a/src/libs/auth/impl/internal/InternalPasswordService.hpp +++ b/src/libs/auth/impl/internal/InternalPasswordService.hpp @@ -36,7 +36,6 @@ namespace Auth InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService); private: - bool checkUserPassword(Database::Session& session, std::string_view loginName, std::string_view password) override; diff --git a/src/libs/auth/impl/pam/PAMPasswordService.cpp b/src/libs/auth/impl/pam/PAMPasswordService.cpp index a9e91569..60623c28 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.cpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.cpp @@ -81,7 +81,6 @@ namespace Auth } private: - class ConvContext { public: @@ -159,7 +158,6 @@ namespace Auth ConvContext* _convContext {}; pam_conv _conv {&PAMContext::conv, this}; pam_handle_t *_pamh {}; - }; bool diff --git a/src/libs/auth/impl/pam/PAMPasswordService.hpp b/src/libs/auth/impl/pam/PAMPasswordService.hpp index 400e0b69..7b418f47 100644 --- a/src/libs/auth/impl/pam/PAMPasswordService.hpp +++ b/src/libs/auth/impl/pam/PAMPasswordService.hpp @@ -31,7 +31,6 @@ namespace Auth using PasswordServiceBase::PasswordServiceBase; private: - bool checkUserPassword(Database::Session& session, std::string_view loginName, std::string_view password) override; diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 2fdd8d10..f6b5217b 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -107,7 +107,6 @@ class LmsApplication : public Wt::WApplication Wt::Signal<>& preQuit() { return _preQuit; } private: - void init(); void setTheme(); void processPasswordAuth(); diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 90e8a326..7662f385 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -493,7 +493,6 @@ SettingsView::refreshView() saveBtn->clicked().connect([=]() { - { auto transaction {LmsApp->getDbSession().createSharedTransaction()};