Added authentication backends: internal, pam and http-headers. fixes #119

This commit is contained in:
emeric
2021-03-04 19:32:09 +01:00
parent b94fe3e852
commit cc28d893f5
73 changed files with 2208 additions and 1254 deletions
+1
View File
@@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True) set(CMAKE_CXX_STANDARD_REQUIRED True)
include(CTest) include(CTest)
find_package(Threads REQUIRED)
find_package(Filesystem REQUIRED) find_package(Filesystem REQUIRED)
find_package(FFMPEGAV REQUIRED) find_package(FFMPEGAV REQUIRED)
find_package(Boost REQUIRED COMPONENTS system program_options) find_package(Boost REQUIRED COMPONENTS system program_options)
+11 -1
View File
@@ -8,6 +8,7 @@
+ [Upgrade](#upgrade) + [Upgrade](#upgrade)
- [Deployment](#deployment) - [Deployment](#deployment)
* [Configuration](#configuration) * [Configuration](#configuration)
* [Authentication backend](#authentication-backend)
* [Deploy on non root path](#deploy-on-non-root-path) * [Deploy on non root path](#deploy-on-non-root-path)
* [Reverse proxy settings](#reverse-proxy-settings) * [Reverse proxy settings](#reverse-proxy-settings)
- [Run](#run) - [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, ...). 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 ## 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`. 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`.
+2 -3
View File
@@ -18,7 +18,6 @@ A [demo instance](http://lms.demo.poupon.io) is available. Note the administrati
* Persistent play queue across sessions * Persistent play queue across sessions
* _Systemd_ integration * _Systemd_ integration
* User management * User management
* with optional _PAM_ authentication backend
* Subsonic API, with the following additional features: * Subsonic API, with the following additional features:
* Playlists * Playlists
* Bookmarks * 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 * Radio mode, based on what is in the current playqueue
* Searches in album, artist and track names (including sort names) * Searches in album, artist and track names (including sort names)
* Starred Albums/Artists/Tracks * 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: * Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like:
* Recently added _Electronic_ artists * Recently added _Electronic_ artists
* Random _Metal_ and _Aggressive_ albums * 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. 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 ## Keyboard shortcuts
* Play/pause: <kbd>Space</bbd> * Play/pause: <kbd>Space</bbd>
+2
View File
@@ -58,11 +58,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-lg-offset-3 col-lg-9"> <div class="col-lg-offset-3 col-lg-9">
${apply-btn class="btn-primary"} ${discard-btn} ${apply-btn class="btn-primary"} ${discard-btn}
</div> </div>
</div> </div>
</div>
</form> </form>
</div> </div>
</div> </div>
-9
View File
@@ -20,15 +20,6 @@
${admin-login-info class="help-block"} ${admin-login-info class="help-block"}
</div> </div>
</div> </div>
<div class="form-group">
<label class="control-label col-lg-3" for="${id:auth-mode}">
${tr:Lms.Admin.User.auth-mode}
</label>
<div class="col-lg-9">
${auth-mode}
${auth-mode-info class="help-block"}
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-lg-3 control-label" for="${id:password}"> <label class="col-lg-3 control-label" for="${id:password}">
${tr:Lms.password} ${tr:Lms.password}
+2 -9
View File
@@ -30,15 +30,7 @@
</div> </div>
</div> </div>
${</if-has-login>} ${</if-has-login>}
<div class="form-group"> ${<if-has-password>}
<label class="control-label col-lg-3" for="${id:auth-mode}">
${tr:Lms.Admin.User.auth-mode}
</label>
<div class="col-lg-9">
${auth-mode}
${auth-mode-info class="help-block"}
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-lg-3" for="${id:password}"> <label class="control-label col-lg-3" for="${id:password}">
${tr:Lms.password-new} ${tr:Lms.password-new}
@@ -48,6 +40,7 @@
${password-info class="help-block"} ${password-info class="help-block"}
</div> </div>
</div> </div>
${</if-has-password>}
${<if-demo>} ${<if-demo>}
<div class="form-group"> <div class="form-group">
<div class="col-lg-offset-3 col-lg-3" for="${id:demo}"> <div class="col-lg-offset-3 col-lg-3" for="${id:demo}">
+2
View File
@@ -6,7 +6,9 @@
<h2>${tr:Lms.Admin.Users.users}</h2> <h2>${tr:Lms.Admin.Users.users}</h2>
</div> </div>
${users} ${users}
${<if-can-create-user>}
${add-btn class="btn-primary Lms-admin-users-add-btn"} ${add-btn class="btn-primary Lms-admin-users-add-btn"}
${</if-can-create-user>}
</message> </message>
<message id="Lms.Admin.Users.template.entry"> <message id="Lms.Admin.Users.template.entry">
+1 -4
View File
@@ -17,7 +17,6 @@
<message id="Lms.password-bad-login-combination">Bad login / password combination</message> <message id="Lms.password-bad-login-combination">Bad login / password combination</message>
<message id="Lms.password-client-throttled">Login throttled, please try again later</message> <message id="Lms.password-client-throttled">Login throttled, please try again later</message>
<message id="Lms.password-confirm">Confirm password</message> <message id="Lms.password-confirm">Confirm password</message>
<message id="Lms.password-must-not-be-empty">Password must not be empty</message>
<message id="Lms.password-new">New password</message> <message id="Lms.password-new">New password</message>
<message id="Lms.password-old">Old password</message> <message id="Lms.password-old">Old password</message>
<message id="Lms.password-too-weak">Password too weak</message> <message id="Lms.password-too-weak">Password too weak</message>
@@ -29,6 +28,7 @@
<!--Errors--> <!--Errors-->
<message id="Lms.Error.artist-not-found">Artist not found</message> <message id="Lms.Error.artist-not-found">Artist not found</message>
<message id="Lms.Error.error-occurred">Error occured!</message> <message id="Lms.Error.error-occurred">Error occured!</message>
<message id="Lms.Error.deployment-error">Deployment error</message>
<message id="Lms.Error.go-home">Go home</message> <message id="Lms.Error.go-home">Go home</message>
<message id="Lms.Error.release-not-found">Release not found</message> <message id="Lms.Error.release-not-found">Release not found</message>
<message id="Lms.Error.user-not-allowed">You are not allowed to perform this operation</message> <message id="Lms.Error.user-not-allowed">You are not allowed to perform this operation</message>
@@ -92,9 +92,6 @@
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message> <message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Users</message>
<!--User--> <!--User-->
<message id="Lms.Admin.User.auth-mode">Authentication mode</message>
<message id="Lms.Admin.User.auth-mode.internal">Internal</message>
<message id="Lms.Admin.User.auth-mode.pam">PAM</message>
<message id="Lms.Admin.User.demo-account">Demo account</message> <message id="Lms.Admin.User.demo-account">Demo account</message>
<message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message> <message id="Lms.Admin.User.demo-account-already-exists">Demo account already exists!</message>
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message> <message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
+1 -4
View File
@@ -17,7 +17,6 @@
<message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message> <message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
<message id="Lms.password-client-throttled">Trop de tentatives de connexion, veuillez réessayer plus tard</message> <message id="Lms.password-client-throttled">Trop de tentatives de connexion, veuillez réessayer plus tard</message>
<message id="Lms.password-confirm">Confirmation du mot de passe</message> <message id="Lms.password-confirm">Confirmation du mot de passe</message>
<message id="Lms.password-must-not-be-empty">Le mot de passe ne doit pas être vide</message>
<message id="Lms.password-new">Nouveau mot de passe</message> <message id="Lms.password-new">Nouveau mot de passe</message>
<message id="Lms.password-old">Ancien mot de passe</message> <message id="Lms.password-old">Ancien mot de passe</message>
<message id="Lms.password-too-weak">Mot de passe trop faible</message> <message id="Lms.password-too-weak">Mot de passe trop faible</message>
@@ -29,6 +28,7 @@
<!--Errors--> <!--Errors-->
<message id="Lms.Error.artist-not-found">Cet artiste n'existe pas</message> <message id="Lms.Error.artist-not-found">Cet artiste n'existe pas</message>
<message id="Lms.Error.error-occurred">Une erreur est survenue!</message> <message id="Lms.Error.error-occurred">Une erreur est survenue!</message>
<message id="Lms.Error.deployment-error">Erreur de déploiement</message>
<message id="Lms.Error.go-home">Retour à l'accueil</message> <message id="Lms.Error.go-home">Retour à l'accueil</message>
<message id="Lms.Error.release-not-found">Cet album n'existe pas</message> <message id="Lms.Error.release-not-found">Cet album n'existe pas</message>
<message id="Lms.Error.user-not-allowed">Vous n'avez pas les droits pour effectuer cette opération</message> <message id="Lms.Error.user-not-allowed">Vous n'avez pas les droits pour effectuer cette opération</message>
@@ -92,9 +92,6 @@
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message> <message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utilisateurs</message>
<!--User--> <!--User-->
<message id="Lms.Admin.User.auth-mode">Mode d'authentification</message>
<message id="Lms.Admin.User.auth-mode.internal">Interne</message>
<message id="Lms.Admin.User.auth-mode.pam">PAM</message>
<message id="Lms.Admin.User.demo-account">Compte de démonstration</message> <message id="Lms.Admin.User.demo-account">Compte de démonstration</message>
<message id="Lms.Admin.User.demo-account-already-exists">Le compte de démonstration existe déjà !</message> <message id="Lms.Admin.User.demo-account-already-exists">Le compte de démonstration existe déjà !</message>
<message id="Lms.Admin.User.demo-password-invalid">Le password doit être égal au login !</message> <message id="Lms.Admin.User.demo-password-invalid">Le password doit être égal au login !</message>
-4
View File
@@ -17,7 +17,6 @@
<message id="Lms.password-bad-login-combination">Errata combinazione di Login / Password </message> <message id="Lms.password-bad-login-combination">Errata combinazione di Login / Password </message>
<message id="Lms.password-client-throttled">Superati i tentativi di accesso, riprova più tardi</message> <message id="Lms.password-client-throttled">Superati i tentativi di accesso, riprova più tardi</message>
<message id="Lms.password-confirm">Conferma la password</message> <message id="Lms.password-confirm">Conferma la password</message>
<message id="Lms.password-must-not-be-empty">Il campo password non deve essere vuoto</message>
<message id="Lms.password-new">Nuova password</message> <message id="Lms.password-new">Nuova password</message>
<message id="Lms.password-old">Vecchia password</message> <message id="Lms.password-old">Vecchia password</message>
<message id="Lms.password-too-weak">La password è troppo debole</message> <message id="Lms.password-too-weak">La password è troppo debole</message>
@@ -92,9 +91,6 @@
<message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utenti</message> <message id="Lms.Admin.Users.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Utenti</message>
<!--User--> <!--User-->
<message id="Lms.Admin.User.auth-mode">Modalità di autenticazione</message>
<message id="Lms.Admin.User.auth-mode.internal">Interna</message>
<message id="Lms.Admin.User.auth-mode.pam">PAM</message>
<message id="Lms.Admin.User.demo-account">Account demo</message> <message id="Lms.Admin.User.demo-account">Account demo</message>
<message id="Lms.Admin.User.demo-account-already-exists">L'account demo è già esistente!</message> <message id="Lms.Admin.User.demo-account-already-exists">L'account demo è già esistente!</message>
<message id="Lms.Admin.User.demo-password-invalid">La password dell'account demo deve essere il nome utente!</message> <message id="Lms.Admin.User.demo-password-invalid">La password dell'account demo deve essere il nome utente!</message>
+4 -1
View File
@@ -131,6 +131,7 @@
</div> </div>
</div> </div>
${</if-has-subsonic-api>} ${</if-has-subsonic-api>}
${<if-has-change-password>}
<legend>${tr:Lms.Settings.change-password}</legend> <legend>${tr:Lms.Settings.change-password}</legend>
<div class="form-horizontal"> <div class="form-horizontal">
${<if-has-old-password>} ${<if-has-old-password>}
@@ -162,7 +163,9 @@
${password-confirm-info class="help-block"} ${password-confirm-info class="help-block"}
</div> </div>
</div> </div>
</div>
${</if-has-change-password>}
<div class="form-horizontal">
<div class="form-group"> <div class="form-group">
<div class="col-lg-offset-3 col-lg-9"> <div class="col-lg-offset-3 col-lg-9">
${apply-btn class="btn-primary"} ${discard-btn} ${apply-btn class="btn-primary"} ${discard-btn}
+8 -3
View File
@@ -37,15 +37,20 @@ http-server-thread-count = 0;
# Acoustic brainz's root API # Acoustic brainz's root API
acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/"; 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
api-subsonic = true; api-subsonic = true;
# Turn on this option to allow the demo account creation/use # Turn on this option to allow the demo account creation/use
demo = false; demo = false;
# Max entries in the login throttler (1 entry per client)
login-throttler-max-entries = 10000;
# Max external cover file size in MBytes # Max external cover file size in MBytes
cover-max-file-size = 10; cover-max-file-size = 10;
+7 -2
View File
@@ -1,8 +1,12 @@
add_library(lmsauth SHARED add_library(lmsauth SHARED
impl/AuthTokenService.cpp impl/AuthTokenService.cpp
impl/PasswordService.cpp impl/AuthServiceBase.cpp
impl/EnvService.cpp
impl/LoginThrottler.cpp impl/LoginThrottler.cpp
impl/PasswordServiceBase.cpp
impl/http-headers/HttpHeadersEnvService.cpp
impl/internal/InternalPasswordService.cpp
) )
target_include_directories(lmsauth INTERFACE target_include_directories(lmsauth INTERFACE
@@ -11,6 +15,7 @@ target_include_directories(lmsauth INTERFACE
target_include_directories(lmsauth PRIVATE target_include_directories(lmsauth PRIVATE
include include
impl
) )
target_link_libraries(lmsauth PRIVATE target_link_libraries(lmsauth PRIVATE
@@ -26,7 +31,7 @@ target_link_libraries(lmsauth PUBLIC
if (USE_PAM) if (USE_PAM)
target_compile_options(lmsauth PRIVATE "-DLMS_SUPPORT_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_include_directories(lmsauth PRIVATE ${PAM_INCLUDE_DIR})
target_link_libraries(lmsauth PRIVATE ${PAM_LIBRARIES}) target_link_libraries(lmsauth PRIVATE ${PAM_LIBRARIES})
endif (USE_PAM) endif (USE_PAM)
+55
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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());
}
}
+38
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
#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);
};
}
+23 -11
View File
@@ -23,12 +23,14 @@
#include <Wt/Auth/PasswordStrengthValidator.h> #include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/WRandom.h> #include <Wt/WRandom.h>
#include "auth/Types.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "utils/Exception.hpp" #include "utils/Exception.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
namespace Auth { namespace Auth
{
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntries) std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntries)
{ {
@@ -52,7 +54,7 @@ AuthTokenService::createAuthToken(Database::Session& session, Database::IdType u
Database::User::pointer user {Database::User::getById(session, userId)}; Database::User::pointer user {Database::User::getById(session, userId)};
if (!user) if (!user)
throw LmsException {"User deleted"}; throw Exception {"User deleted"};
Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, expiry, user)}; Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, expiry, user)};
@@ -66,9 +68,9 @@ AuthTokenService::createAuthToken(Database::Session& session, Database::IdType u
static static
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
processAuthToken(Database::Session& session, const std::string& secret) processAuthToken(Database::Session& session, std::string_view secret)
{ {
const std::string secretHash {sha1Function.compute(secret, {})}; const std::string secretHash {sha1Function.compute(std::string {secret}, {})};
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
@@ -90,13 +92,12 @@ processAuthToken(Database::Session& session, const std::string& secret)
return res; return res;
} }
AuthTokenService::AuthTokenProcessResult AuthTokenService::AuthTokenProcessResult
AuthTokenService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) AuthTokenService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
{ {
// Do not waste too much resource on brute force attacks (optim) // Do not waste too much resource on brute force attacks (optim)
{ {
std::shared_lock<std::shared_timed_mutex> lock {_mutex}; std::shared_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress)) if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled}; return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
@@ -104,7 +105,7 @@ AuthTokenService::processAuthToken(Database::Session& session, const boost::asio
auto res {Auth::processAuthToken(session, tokenValue)}; auto res {Auth::processAuthToken(session, tokenValue)};
{ {
std::unique_lock<std::shared_timed_mutex> lock {_mutex}; std::unique_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress)) if (_loginThrottler.isClientThrottled(clientAddress))
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled}; return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
@@ -112,14 +113,25 @@ AuthTokenService::processAuthToken(Database::Session& session, const boost::asio
if (!res) if (!res)
{ {
_loginThrottler.onBadClientAttempt(clientAddress); _loginThrottler.onBadClientAttempt(clientAddress);
return AuthTokenProcessResult {AuthTokenProcessResult::State::NotFound}; return AuthTokenProcessResult {AuthTokenProcessResult::State::Denied};
} }
_loginThrottler.onGoodClientAttempt(clientAddress); _loginThrottler.onGoodClientAttempt(clientAddress);
return AuthTokenProcessResult {AuthTokenProcessResult::State::Found, std::move(*res)}; 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()};
Database::User::pointer user {Database::User::getById(session, userId)};
if (!user)
throw Exception {"User deleted"};
user.modify()->clearAuthTokens();
}
} // namespace Auth } // namespace Auth
+10 -12
View File
@@ -19,8 +19,10 @@
#pragma once #pragma once
#include "auth/IAuthTokenService.hpp" #include <shared_mutex>
#include "auth/IAuthTokenService.hpp"
#include "AuthServiceBase.hpp"
#include "LoginThrottler.hpp" #include "LoginThrottler.hpp"
namespace Database namespace Database
@@ -28,13 +30,11 @@ namespace Database
class Session; class Session;
} }
namespace Auth
namespace Auth { {
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
class AuthTokenService : public IAuthTokenService
{ {
public: public:
AuthTokenService(std::size_t maxThrottlerEntries); AuthTokenService(std::size_t maxThrottlerEntries);
AuthTokenService(const AuthTokenService&) = delete; AuthTokenService(const AuthTokenService&) = delete;
@@ -42,14 +42,12 @@ namespace Auth {
AuthTokenService(AuthTokenService&&) = delete; AuthTokenService(AuthTokenService&&) = delete;
AuthTokenService& operator=(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: 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; std::shared_mutex _mutex;
LoginThrottler _loginThrottler; LoginThrottler _loginThrottler;
}; };
} }
+35
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "auth/IEnvService.hpp"
#include "auth/Types.hpp"
#include "http-headers/HttpHeadersEnvService.hpp"
namespace Auth
{
std::unique_ptr<IEnvService>
createEnvService(std::string_view backendName)
{
if (backendName == "http-headers")
return std::make_unique<HttpHeadersEnvService>();
throw Exception {"Authentication backend '" + std::string {backendName} + "' is not supported!"};
}
}
-156
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PasswordService.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/WRandom.h>
#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<IPasswordService> createPasswordService(std::size_t maxThrottlerEntries)
{
return std::make_unique<PasswordService>(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<std::shared_timed_mutex> lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return PasswordCheckResult::Throttled;
}
const bool match {Auth::checkUserPassword(session, loginName, password)};
{
std::unique_lock<std::shared_timed_mutex> 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
-57
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#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;
};
}
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PasswordServiceBase.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/WRandom.h>
#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<IPasswordService>
createPasswordService(std::string_view passwordAuthenticationBackend, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
{
if (passwordAuthenticationBackend == "internal")
return std::make_unique<InternalPasswordService>(maxThrottlerEntries, authTokenService);
#ifdef LMS_SUPPORT_PAM
else if (passwordAuthenticationBackend == "pam")
return std::make_unique<PAMPasswordService>(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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "HttpHeadersEnvService.hpp"
#include <Wt/WEnvironment.h>
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
namespace Auth
{
HttpHeadersEnvService::HttpHeadersEnvService()
: _fieldName {Service<IConfig>::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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "InternalPasswordService.hpp"
#include <Wt/WRandom.h>
#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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#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;
};
}
-183
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PAM.hpp"
#include <cstring>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include <security/pam_appl.h>
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<PAMContext*>(userData)};
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(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<pam_response*>(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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "PAMPasswordService.hpp"
#ifndef LMS_SUPPORT_PAM
#error "Should not compile this"
#endif
#include <cstring>
#include <security/pam_appl.h>
#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<PAMContext*>(userData)};
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(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<pam_response*>(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
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#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;
};
}
@@ -23,6 +23,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view>
#include <boost/asio/ip/address.hpp> #include <boost/asio/ip/address.hpp>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
@@ -31,11 +32,11 @@
namespace Database namespace Database
{ {
class Session; class Session;
class User;
} }
namespace Auth
namespace Auth { {
class IAuthTokenService class IAuthTokenService
{ {
public: public:
@@ -46,9 +47,9 @@ namespace Auth {
{ {
enum class State enum class State
{ {
Found, Granted,
Throttled, Throttled,
NotFound, Denied,
}; };
struct AuthTokenInfo struct AuthTokenInfo
@@ -57,16 +58,17 @@ namespace Auth {
Wt::WDateTime expiry; Wt::WDateTime expiry;
}; };
State state {State::NotFound}; State state {State::Denied};
std::optional<AuthTokenInfo> authTokenInfo {}; std::optional<AuthTokenInfo> authTokenInfo {};
}; };
// Removed if found // Provided token is only accepted once
virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) = 0; 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 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<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntryCount); std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntryCount);
} }
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#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<Database::IdType> userId {};
};
virtual CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) = 0;
};
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName);
} // namespace Auth
+31 -16
View File
@@ -19,44 +19,59 @@
#pragma once #pragma once
#include <optional> #include <string_view>
#include <string>
#include <Wt/WDateTime.h>
#include <Wt/Dbo/ptr.h>
#include <boost/asio/ip/address.hpp> #include <boost/asio/ip/address.hpp>
#include "auth/Types.hpp"
#include "database/User.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
namespace Database namespace Database
{ {
class Session; class Session;
class User;
} }
namespace Auth
{
namespace Auth { class IAuthTokenService;
class IPasswordService class IPasswordService
{ {
public: public:
virtual ~IPasswordService() = default; virtual ~IPasswordService() = default;
// Password services struct CheckResult
enum class PasswordCheckResult
{ {
Match, enum class State
Mismatch, {
Granted,
Denied,
Throttled, Throttled,
}; };
State state {State::Denied};
std::optional<Database::IdType> userId {};
std::optional<Wt::WDateTime> 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
{
virtual PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) = 0; public:
virtual Database::User::PasswordHash hashPassword(const std::string& password) const = 0; PasswordTooWeakException() : Auth::Exception {"Password too weak"} {}
virtual bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const = 0;
}; };
std::unique_ptr<IPasswordService> createPasswordService(std::size_t maxThrottlerEntryCount); 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<IPasswordService> createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
} }
+37
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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"} {}
};
}
+1 -2
View File
@@ -9,6 +9,7 @@ target_include_directories(lmscover INTERFACE
target_include_directories(lmscover PRIVATE target_include_directories(lmscover PRIVATE
include include
impl
) )
target_link_libraries(lmscover PRIVATE target_link_libraries(lmscover PRIVATE
@@ -39,7 +40,5 @@ else ()
message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided") message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided")
endif() endif()
target_include_directories(lmscover PRIVATE impl)
install(TARGETS lmscover DESTINATION lib) install(TARGETS lmscover DESTINATION lib)
+32 -25
View File
@@ -22,6 +22,7 @@
#include <map> #include <map>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include <string_view>
#include "utils/Exception.hpp" #include "utils/Exception.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -40,9 +41,8 @@
namespace Database { namespace Database {
#define LMS_DATABASE_VERSION 28
using Version = std::size_t; using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION {29};
class VersionInfo class VersionInfo
{ {
@@ -270,7 +270,7 @@ CREATE TABLE "user_backup" (
else if (version == 24) else if (version == 24)
{ {
// User's AuthMode // User's AuthMode
_session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultAuthMode)) + ")"); _session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultAuthMode*/0)) + ")");
} }
else if (version == 25) 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 // Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion(); 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 else
{ {
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
@@ -329,46 +354,28 @@ enum class OwnedLock
Unique, Unique,
}; };
static thread_local std::map<std::shared_mutex*, OwnedLock> lockDebug; UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
UniqueTransaction::UniqueTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session)
: _lock {mutex}, : _lock {mutex},
_transaction {session} _transaction {session}
{ {
assert(lockDebug[_lock.mutex()] == OwnedLock::None);
lockDebug[_lock.mutex()] = OwnedLock::Unique;
} }
UniqueTransaction::~UniqueTransaction() SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
{
assert(lockDebug[_lock.mutex()] == OwnedLock::Unique);
lockDebug[_lock.mutex()] = OwnedLock::None;
}
SharedTransaction::SharedTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session)
: _lock {mutex}, : _lock {mutex},
_transaction {session} _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 void
Session::checkUniqueLocked() Session::checkUniqueLocked()
{ {
assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique); // assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique);
} }
void void
Session::checkSharedLocked() Session::checkSharedLocked()
{ {
assert(lockDebug[&_db.getMutex()] != OwnedLock::None); // assert(lockDebug[&_db.getMutex()] != OwnedLock::None);
} }
UniqueTransaction UniqueTransaction
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
namespace Wt::Dbo
{
template<>
struct sql_value_traits<std::string_view>
{
static void bind(std::string_view str, SqlStatement *statement, int column, int /* size */)
{
statement->bind(column, std::string {str});
}
};
}
+12 -3
View File
@@ -25,6 +25,7 @@
#include "database/Track.hpp" #include "database/Track.hpp"
#include "database/TrackList.hpp" #include "database/TrackList.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "StringViewTraits.hpp"
namespace Database { 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 playedListName {"__played_tracks__"};
static const std::string queuedListName {"__queued_tracks__"}; static const std::string queuedListName {"__queued_tracks__"};
User::User(const std::string& loginName) User::User(std::string_view loginName)
: _loginName {loginName} : _loginName {loginName}
{ {
} }
@@ -93,8 +94,16 @@ User::getDemo(Session& session)
return res; return res;
} }
std::size_t
User::getCount(Session& session)
{
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
}
User::pointer User::pointer
User::create(Session& session, const std::string& loginName) User::create(Session& session, std::string_view loginName)
{ {
session.checkUniqueLocked(); session.checkUniqueLocked();
@@ -115,7 +124,7 @@ User::getById(Session& session, IdType id)
} }
User::pointer User::pointer
User::getByLoginName(Session& session, const std::string& name) User::getByLoginName(Session& session, std::string_view name)
{ {
return session.getDboSession().find<User>() return session.getDboSession().find<User>()
.where("login_name = ?").bind(name); .where("login_name = ?").bind(name);
+4 -3
View File
@@ -20,10 +20,11 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <shared_mutex>
#include <Wt/Dbo/SqlConnectionPool.h> #include <Wt/Dbo/SqlConnectionPool.h>
#include "utils/RecursiveSharedMutex.hpp"
namespace Database { namespace Database {
class Session; class Session;
@@ -44,7 +45,7 @@ class Db
private: private:
friend class Session; friend class Session;
std::shared_mutex& getMutex() { return _sharedMutex; } RecursiveSharedMutex& getMutex() { return _sharedMutex; }
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; } Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
class ScopedConnection class ScopedConnection
@@ -88,7 +89,7 @@ class Db
void executeSql(const std::string& sql); void executeSql(const std::string& sql);
std::shared_mutex _sharedMutex; RecursiveSharedMutex _sharedMutex;
std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool; std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool;
std::mutex _tlsSessionsMutex; std::mutex _tlsSessionsMutex;
+9 -15
View File
@@ -19,40 +19,34 @@
#pragma once #pragma once
#include <mutex>
#include <map>
#include <memory> #include <memory>
#include <shared_mutex> #include <mutex>
#include <vector>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include <Wt/Dbo/SqlConnectionPool.h> #include <Wt/Dbo/SqlConnectionPool.h>
namespace Database { #include "utils/RecursiveSharedMutex.hpp"
namespace Database
{
class UniqueTransaction class UniqueTransaction
{ {
public:
~UniqueTransaction();
private: private:
friend class Session; friend class Session;
UniqueTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session); UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
std::unique_lock<std::shared_mutex> _lock; std::unique_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction; Wt::Dbo::Transaction _transaction;
}; };
class SharedTransaction class SharedTransaction
{ {
public:
~SharedTransaction();
private: private:
friend class Session; friend class Session;
SharedTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session); SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
std::shared_lock<std::shared_mutex> _lock; std::shared_lock<RecursiveSharedMutex> _lock;
Wt::Dbo::Transaction _transaction; Wt::Dbo::Transaction _transaction;
}; };
+5 -14
View File
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <optional> #include <optional>
#include <string_view>
#include <vector> #include <vector>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
@@ -100,12 +101,6 @@ class User : public Wt::Dbo::Dbo<User>
DEMO = 2, DEMO = 2,
}; };
enum class AuthMode
{
Internal = 0,
PAM = 1,
};
struct PasswordHash struct PasswordHash
{ {
std::string salt; std::string salt;
@@ -144,19 +139,19 @@ class User : public Wt::Dbo::Dbo<User>
static inline const Bitrate defaultSubsonicTranscodeBitrate {128000}; static inline const Bitrate defaultSubsonicTranscodeBitrate {128000};
static inline const UITheme defaultUITheme {UITheme::Dark}; static inline const UITheme defaultUITheme {UITheme::Dark};
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists}; static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists};
static inline const AuthMode defaultAuthMode {AuthMode::Internal};
User() = default; User() = default;
User(const std::string& loginName); User(std::string_view loginName);
// utility // 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 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<pointer> getAll(Session& session); static std::vector<pointer> getAll(Session& session);
static pointer getDemo(Session& session); static pointer getDemo(Session& session);
static std::size_t getCount(Session& session);
// accessors // accessors
const std::string& getLoginName() const { return _loginName; } const std::string& getLoginName() const { return _loginName; }
@@ -173,7 +168,6 @@ class User : public Wt::Dbo::Dbo<User>
void setSubsonicTranscodeBitrate(Bitrate bitrate); void setSubsonicTranscodeBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; } void setRadio(bool val) { _radio = val; }
void setAuthMode(AuthMode mode) { _authMode = mode;}
void setRepeatAll(bool val) { _repeatAll = val; } void setRepeatAll(bool val) { _repeatAll = val; }
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; } void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens(); void clearAuthTokens();
@@ -188,7 +182,6 @@ class User : public Wt::Dbo::Dbo<User>
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; } bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; } bool isRadioSet() const { return _radio; }
AuthMode getAuthMode() const { return _authMode; }
UITheme getUITheme() const { return _uiTheme; } UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; } SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
@@ -225,7 +218,6 @@ class User : public Wt::Dbo::Dbo<User>
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos"); Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::field(a, _repeatAll, "repeat_all"); Wt::Dbo::field(a, _repeatAll, "repeat_all");
Wt::Dbo::field(a, _radio, "radio"); 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, _tracklists, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade); 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); Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
@@ -254,7 +246,6 @@ class User : public Wt::Dbo::Dbo<User>
int _curPlayingTrackPos {}; // Current track position in queue int _curPlayingTrackPos {}; // Current track position in queue
bool _repeatAll {}; bool _repeatAll {};
bool _radio {}; bool _radio {};
AuthMode _authMode {defaultAuthMode};
Wt::Dbo::collection<Wt::Dbo::ptr<TrackList>> _tracklists; Wt::Dbo::collection<Wt::Dbo::ptr<TrackList>> _tracklists;
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _starredArtists; Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _starredArtists;
@@ -38,9 +38,9 @@ static
std::string std::string
getJsonData(const UUID& mbid) 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<IConfig>::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"}; const std::string url {std::string {Service<IConfig>::get()->getString("acousticbrainz-api-url", defaultAPIURL)} + std::string {mbid.getAsString()} + "/low-level"};
boost::asio::io_service ioService; boost::asio::io_service ioService;
+91 -37
View File
@@ -105,6 +105,15 @@ namespace StringUtils
namespace API::Subsonic namespace API::Subsonic
{ {
static
void
checkSetPasswordImplemented()
{
Auth::IPasswordService* passwordService {Service<Auth::IPasswordService>::get()};
if (!passwordService || !passwordService->canSetPasswords())
throw NotImplementedGenericError {};
}
static static
std::string std::string
makeNameFilesystemCompatible(const std::string& name) makeNameFilesystemCompatible(const std::string& name)
@@ -515,12 +524,11 @@ handleChangePassword(RequestContext& context)
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")}; std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))}; std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password)) try
throw PasswordTooWeakGenericError {}; {
Database::IdType userId;
const User::PasswordHash hash {Service<Auth::IPasswordService>::get()->hashPassword(password)}; {
auto transaction {context.dbSession.createSharedTransaction()};
auto transaction {context.dbSession.createUniqueTransaction()};
checkUserIsMySelfOrAdmin(context, username); checkUserIsMySelfOrAdmin(context, username);
@@ -528,8 +536,19 @@ handleChangePassword(RequestContext& context)
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw UserNotAuthorizedError {};
user.modify()->setPasswordHash(hash); userId = user.id();
user.modify()->clearAuthTokens(); }
Service<Auth::IPasswordService>::get()->setPassword(context.dbSession, userId, password);
}
catch (Auth::IPasswordService::PasswordTooWeakException&)
{
throw PasswordTooWeakGenericError {};
}
catch (Auth::Exception& authException)
{
throw UserNotAuthorizedError {};
}
return Response::createOkResponse(context); return Response::createOkResponse(context);
} }
@@ -597,19 +616,40 @@ handleCreateUserRequest(RequestContext& context)
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))}; std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
// Just ignore all the other fields as we don't handle them // Just ignore all the other fields as we don't handle them
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password)) Database::IdType userId;
throw PasswordTooWeakGenericError {}; {
const User::PasswordHash hash {Service<Auth::IPasswordService>::get()->hashPassword(password)};
auto transaction {context.dbSession.createUniqueTransaction()}; auto transaction {context.dbSession.createUniqueTransaction()};
if (User::getByLoginName(context.dbSession, username) != User::pointer{}) User::pointer user {User::getByLoginName(context.dbSession, username)};
if (user)
throw UserAlreadyExistsGenericError {}; throw UserAlreadyExistsGenericError {};
User::pointer user {User::create(context.dbSession, username)}; user = User::create(context.dbSession, username);
user.modify()->setAuthMode(User::AuthMode::Internal); userId = user.id();
user.modify()->setPasswordHash(hash); }
auto removeCreatedUser {[&]()
{
auto transaction {context.dbSession.createUniqueTransaction()};
User::pointer user {User::getById(context.dbSession, userId)};
if (user)
user.remove();
}};
try
{
Service<Auth::IPasswordService>::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); return Response::createOkResponse(context);
} }
@@ -1614,26 +1654,33 @@ handleUpdateUserRequest(RequestContext& context)
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")}; std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
std::optional<std::string> password {getParameterAs<std::string>(context.parameters, "password")}; std::optional<std::string> password {getParameterAs<std::string>(context.parameters, "password")};
User::PasswordHash hash; Database::IdType userId;
if (password)
{ {
*password = decodePasswordIfNeeded(*password); auto transaction {context.dbSession.createSharedTransaction()};
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, *password))
throw PasswordTooWeakGenericError {};
hash = Service<Auth::IPasswordService>::get()->hashPassword(*password);
}
auto transaction {context.dbSession.createUniqueTransaction()};
User::pointer user {User::getByLoginName(context.dbSession, username)}; User::pointer user {User::getByLoginName(context.dbSession, username)};
if (!user) if (!user)
throw UserNotAuthorizedError {}; throw RequestedDataNotFoundError {};
userId = user.id();
}
if (password) if (password)
{ {
user.modify()->setPasswordHash(hash); checkSetPasswordImplemented();
user.modify()->clearAuthTokens();
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); return Response::createOkResponse(context);
@@ -1826,10 +1873,12 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/,
} }
using RequestHandlerFunc = std::function<Response(RequestContext& context)>; using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
using CheckImplementedFunc = std::function<void()>;
struct RequestEntryPointInfo struct RequestEntryPointInfo
{ {
RequestHandlerFunc func; RequestHandlerFunc func;
bool mustBeAdmin; bool mustBeAdmin;
CheckImplementedFunc checkFunc {};
}; };
static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
@@ -1920,10 +1969,10 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
// User management // User management
{"getUser", {handleGetUserRequest, false}}, {"getUser", {handleGetUserRequest, false}},
{"getUsers", {handleGetUsersRequest, true}}, {"getUsers", {handleGetUsersRequest, true}},
{"createUser", {handleCreateUserRequest, true}}, {"createUser", {handleCreateUserRequest, true, &checkSetPasswordImplemented}},
{"updateUser", {handleUpdateUserRequest, true}}, {"updateUser", {handleUpdateUserRequest, true}},
{"deleteUser", {handleDeleteUserRequest, true}}, {"deleteUser", {handleDeleteUserRequest, true}},
{"changePassword", {handleChangePassword, false}}, {"changePassword", {handleChangePassword, false, &checkSetPasswordImplemented}},
// Bookmarks // Bookmarks
{"getBookmarks", {handleGetBookmarks, false}}, {"getBookmarks", {handleGetBookmarks, false}},
@@ -1975,15 +2024,17 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
Session& dbSession {_db.getTLSSession()}; Session& dbSession {_db.getTLSSession()};
switch (Service<Auth::IPasswordService>::get()->checkUserPassword(dbSession, const Auth::IPasswordService::CheckResult checkResult {Service<Auth::IPasswordService>::get()->checkUserPassword(dbSession,
boost::asio::ip::address::from_string(request.clientAddress()), boost::asio::ip::address::from_string(request.clientAddress()),
clientInfo.user, clientInfo.password)) clientInfo.user, clientInfo.password)};
switch (checkResult.state)
{ {
case Auth::IPasswordService::PasswordCheckResult::Match: case Auth::IPasswordService::CheckResult::State::Granted:
break; break;
case Auth::IPasswordService::PasswordCheckResult::Mismatch: case Auth::IPasswordService::CheckResult::State::Denied:
throw WrongUsernameOrPasswordError {}; throw WrongUsernameOrPasswordError {};
case Auth::IPasswordService::PasswordCheckResult::Throttled: case Auth::IPasswordService::CheckResult::State::Throttled:
throw LoginThrottledGenericError {}; throw LoginThrottledGenericError {};
} }
@@ -1992,6 +2043,9 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
auto itEntryPoint {requestEntryPoints.find(requestPath)}; auto itEntryPoint {requestEntryPoints.find(requestPath)};
if (itEntryPoint != requestEntryPoints.end()) if (itEntryPoint != requestEntryPoints.end())
{ {
if (itEntryPoint->second.checkFunc)
itEntryPoint->second.checkFunc();
if (itEntryPoint->second.mustBeAdmin) if (itEntryPoint->second.mustBeAdmin)
{ {
auto transaction {dbSession.createSharedTransaction()}; auto transaction {dbSession.createSharedTransaction()};
+1
View File
@@ -8,6 +8,7 @@ add_library(lmsutils SHARED
impl/NetAddress.cpp impl/NetAddress.cpp
impl/Path.cpp impl/Path.cpp
impl/Random.cpp impl/Random.cpp
impl/RecursiveSharedMutex.cpp
impl/StreamLogger.cpp impl/StreamLogger.cpp
impl/String.cpp impl/String.cpp
impl/UUID.cpp impl/UUID.cpp
+16 -24
View File
@@ -47,70 +47,62 @@ Config::Config(const std::filesystem::path& p)
} }
} }
std::string std::string_view
Config::getString(const std::string& setting, const std::string& def, const std::unordered_set<std::string>& allowedValues) Config::getString(std::string_view setting, std::string_view def)
{ {
try { try {
std::string res {(const char*)_config.lookup(setting)}; return static_cast<const char*>(_config.lookup(std::string {setting}));
if (!allowedValues.empty() && allowedValues.find(res) == std::cend(allowedValues))
{
LMS_LOG(MAIN, ERROR) << "Invalid setting for '" << setting << "', using default value '" << def << "'";
return def;
} }
catch (libconfig::ConfigException&)
return res;
}
catch (std::exception &e)
{ {
return def; return def;
} }
} }
std::filesystem::path 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 { try {
const char* res = _config.lookup(setting); const char* res {_config.lookup(std::string {setting})};
return std::filesystem::path {std::string(res)}; return std::filesystem::path {std::string(res)};
} }
catch (std::exception &e) catch (libconfig::ConfigException&)
{ {
return path; return path;
} }
} }
unsigned long unsigned long
Config::getULong(const std::string& setting, unsigned long def) Config::getULong(std::string_view setting, unsigned long def)
{ {
try { try {
return static_cast<unsigned int>(_config.lookup(setting)); return static_cast<unsigned int>(_config.lookup(std::string {setting}));
} }
catch (...) catch (libconfig::ConfigException&)
{ {
return def; return def;
} }
} }
long long
Config::getLong(const std::string& setting, long def) Config::getLong(std::string_view setting, long def)
{ {
try { try {
return _config.lookup(setting); return _config.lookup(std::string {setting});
} }
catch (...) catch (libconfig::ConfigException&)
{ {
return def; return def;
} }
} }
bool bool
Config::getBool(const std::string& setting, bool def) Config::getBool(std::string_view setting, bool def)
{ {
try { try {
return _config.lookup(setting); return _config.lookup(std::string {setting});
} }
catch (...) catch (libconfig::ConfigException&)
{ {
return def; return def;
} }
+5 -5
View File
@@ -35,11 +35,11 @@ class Config final : public IConfig
Config& operator=(Config&&) = delete; Config& operator=(Config&&) = delete;
// Default values are returned in case of setting not found // 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<std::string>& allowedValues = {}) override; std::string_view getString(std::string_view setting, std::string_view def = "") override;
std::filesystem::path getPath(const std::string& setting, const std::filesystem::path& def = std::filesystem::path()) override; std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override;
unsigned long getULong(const std::string& setting, unsigned long def = 0) override; unsigned long getULong(std::string_view setting, unsigned long def = 0) override;
long getLong(const std::string& setting, long def = 0) override; long getLong(std::string_view setting, long def = 0) override;
bool getBool(const std::string& setting, bool def = false) override; bool getBool(std::string_view setting, bool def = false) override;
private: private:
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "utils/RecursiveSharedMutex.hpp"
#include <cassert>
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();
}
+6 -7
View File
@@ -18,9 +18,8 @@
*/ */
#pragma once #pragma once
#include <string_view>
#include <filesystem> #include <filesystem>
#include <memory>
#include <unordered_set>
// Used to get config values from configuration files // Used to get config values from configuration files
class IConfig class IConfig
@@ -30,11 +29,11 @@ class IConfig
virtual ~IConfig() = default; virtual ~IConfig() = default;
// Default values are returned in case of setting not found // 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<std::string>& allowedValues = {}) = 0; virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0;
virtual std::filesystem::path getPath(const std::string& setting, const std::filesystem::path& def = std::filesystem::path()) = 0; virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0;
virtual unsigned long getULong(const std::string& setting, unsigned long def = 0) = 0; virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0;
virtual long getLong(const std::string& setting, long def = 0) = 0; virtual long getLong(std::string_view setting, long def = 0) = 0;
virtual bool getBool(const std::string& setting, bool def = false) = 0; virtual bool getBool(std::string_view setting, bool def = false) = 0;
}; };
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <mutex>
#include <shared_mutex>
#include <thread>
#include <unordered_map>
// 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<std::thread::id, std::size_t> _sharedCounts;
};
+6 -2
View File
@@ -26,6 +26,7 @@ template <typename Class>
class Service class Service
{ {
public: public:
Service() = default;
Service(std::unique_ptr<Class> service) Service(std::unique_ptr<Class> service)
{ {
assign(std::move(service)); assign(std::move(service));
@@ -52,14 +53,17 @@ class Service
} }
static Class* get() { return _service.get(); } static Class* get() { return _service.get(); }
static bool exists() { return _service.get(); }
private: template <typename SubClass>
static Class& assign(std::unique_ptr<Class> service) static Class& assign(std::unique_ptr<SubClass> service)
{ {
assert(!_service); assert(!_service);
_service = std::move(service); _service = std::move(service);
return *get(); return *get();
} }
private:
static void clear() { _service.reset(); } static void clear() { _service.reset(); }
static inline std::unique_ptr<Class> _service; static inline std::unique_ptr<Class> _service;
+4 -2
View File
@@ -14,9 +14,11 @@ add_executable(lms
ui/admin/InitWizardView.cpp ui/admin/InitWizardView.cpp
ui/admin/UserView.cpp ui/admin/UserView.cpp
ui/admin/UsersView.cpp ui/admin/UsersView.cpp
ui/common/AuthModeModel.cpp ui/common/DirectoryValidator.cpp
ui/common/LoadingIndicator.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/ArtistListHelpers.cpp
ui/explore/ArtistView.cpp ui/explore/ArtistView.cpp
ui/explore/ArtistsView.cpp ui/explore/ArtistsView.cpp
+29 -11
View File
@@ -26,6 +26,7 @@
#include "auth/IAuthTokenService.hpp" #include "auth/IAuthTokenService.hpp"
#include "auth/IPasswordService.hpp" #include "auth/IPasswordService.hpp"
#include "auth/IEnvService.hpp"
#include "cover/ICoverArtGrabber.hpp" #include "cover/ICoverArtGrabber.hpp"
#include "database/Db.hpp" #include "database/Db.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -36,6 +37,7 @@
#include "utils/IChildProcessManager.hpp" #include "utils/IChildProcessManager.hpp"
#include "utils/IConfig.hpp" #include "utils/IConfig.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "utils/String.hpp"
#include "utils/WtLogger.hpp" #include "utils/WtLogger.hpp"
static static
@@ -52,24 +54,24 @@ generateWtConfig(std::string execPath)
args.push_back(execPath); args.push_back(execPath);
args.push_back("--config=" + wtConfigPath.string()); args.push_back("--config=" + wtConfigPath.string());
args.push_back("--docroot=" + Service<IConfig>::get()->getString("docroot")); args.push_back("--docroot=" + std::string {Service<IConfig>::get()->getString("docroot")});
args.push_back("--approot=" + Service<IConfig>::get()->getString("approot")); args.push_back("--approot=" + std::string {Service<IConfig>::get()->getString("approot")});
args.push_back("--deploy-path=" + Service<IConfig>::get()->getString("deploy-path", "/")); args.push_back("--deploy-path=" + std::string {Service<IConfig>::get()->getString("deploy-path", "/")});
if (!wtResourcesPath.empty()) if (!wtResourcesPath.empty())
args.push_back("--resources-dir=" + wtResourcesPath.string()); args.push_back("--resources-dir=" + wtResourcesPath.string());
if (Service<IConfig>::get()->getBool("tls-enable", false)) if (Service<IConfig>::get()->getBool("tls-enable", false))
{ {
args.push_back("--https-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082))); args.push_back("--https-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
args.push_back("--https-address=" + Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")); args.push_back("--https-address=" + std::string {Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")});
args.push_back("--ssl-certificate=" + Service<IConfig>::get()->getString("tls-cert")); args.push_back("--ssl-certificate=" + std::string {Service<IConfig>::get()->getString("tls-cert")});
args.push_back("--ssl-private-key=" + Service<IConfig>::get()->getString("tls-key")); args.push_back("--ssl-private-key=" + std::string {Service<IConfig>::get()->getString("tls-key")});
args.push_back("--ssl-tmp-dh=" + Service<IConfig>::get()->getString("tls-dh")); args.push_back("--ssl-tmp-dh=" + std::string {Service<IConfig>::get()->getString("tls-dh")});
} }
else else
{ {
args.push_back("--http-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082))); args.push_back("--http-port=" + std::to_string( Service<IConfig>::get()->getULong("listen-port", 5082)));
args.push_back("--http-address=" + Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")); args.push_back("--http-address=" + std::string {Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")});
} }
if (!wtAccessLogFilePath.empty()) if (!wtAccessLogFilePath.empty())
@@ -211,10 +213,26 @@ int main(int argc, char* argv[])
UserInterface::LmsApplicationGroupContainer appGroups; UserInterface::LmsApplicationGroupContainer appGroups;
// Service initialization order is important // Service initialization order is important (reverse-order for deinit)
Service<IChildProcessManager> childProcessManagerService {createChildProcessManager()}; Service<IChildProcessManager> childProcessManagerService {createChildProcessManager()};
Service<Auth::IAuthTokenService> authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))};
Service<Auth::IPasswordService> passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))}; Service<Auth::IAuthTokenService> authTokenService;
Service<Auth::IPasswordService> authPasswordService;
Service<Auth::IEnvService> 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<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(argv[0], Service<CoverArt::IGrabber> coverArtService {CoverArt::createGrabber(argv[0],
server.appRoot() + "/images/unknown-cover.jpg", server.appRoot() + "/images/unknown-cover.jpg",
config->getULong("cover-max-cache-size", 30) * 1000 * 1000, config->getULong("cover-max-cache-size", 30) * 1000 * 1000,
+16 -10
View File
@@ -30,13 +30,17 @@
#include "auth/IAuthTokenService.hpp" #include "auth/IAuthTokenService.hpp"
#include "auth/IPasswordService.hpp" #include "auth/IPasswordService.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "common/Validators.hpp" #include "common/LoginNameValidator.hpp"
#include "common/MandatoryValidator.hpp"
#include "common/PasswordValidator.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
namespace UserInterface { namespace UserInterface
{
static const std::string authCookieName {"LmsAuth"}; 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)}; const auto res {Service<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)};
switch (res.state) switch (res.state)
{ {
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::NotFound: case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled: case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
LmsApp->setCookie(authCookieName, std::string {}, 0, "", "", env.urlScheme() == "https"); LmsApp->setCookie(authCookieName, std::string {}, 0, "", "", env.urlScheme() == "https");
return std::nullopt; return std::nullopt;
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Found: case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry); createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry);
break; break;
} }
@@ -93,7 +97,7 @@ class AuthModel : public Wt::WFormModel
addField(PasswordField); addField(PasswordField);
addField(RememberMeField); addField(RememberMeField);
setValidator(LoginNameField, createNameValidator()); setValidator(LoginNameField, createLoginNameValidator());
setValidator(PasswordField, createMandatoryValidator()); setValidator(PasswordField, createMandatoryValidator());
} }
@@ -125,18 +129,20 @@ class AuthModel : public Wt::WFormModel
if (field == PasswordField) if (field == PasswordField)
{ {
switch (Service<::Auth::IPasswordService>::get()->checkUserPassword( const auto checkResult {Service<::Auth::IPasswordService>::get()->checkUserPassword(
LmsApp->getDbSession(), LmsApp->getDbSession(),
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
valueText(LoginNameField).toUTF8(), 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; break;
case ::Auth::IPasswordService::PasswordCheckResult::Mismatch: case ::Auth::IPasswordService::CheckResult::State::Denied:
error = Wt::WString::tr("Lms.password-bad-login-combination"); error = Wt::WString::tr("Lms.password-bad-login-combination");
break; break;
case ::Auth::IPasswordService::PasswordCheckResult::Throttled: case ::Auth::IPasswordService::CheckResult::State::Throttled:
error = Wt::WString::tr("Lms.password-client-throttled"); error = Wt::WString::tr("Lms.password-client-throttled");
break; break;
} }
-3
View File
@@ -36,7 +36,4 @@ class Auth : public Wt::WTemplateFormView
Wt::Signal<Database::IdType /*userId*/> userLoggedIn; Wt::Signal<Database::IdType /*userId*/> userLoggedIn;
}; };
} // namespace UserInterface } // namespace UserInterface
+86 -66
View File
@@ -28,6 +28,8 @@
#include <Wt/WStackedWidget.h> #include <Wt/WStackedWidget.h>
#include <Wt/WText.h> #include <Wt/WText.h>
#include "auth/IEnvService.hpp"
#include "auth/IPasswordService.hpp"
#include "cover/ICoverArtGrabber.hpp" #include "cover/ICoverArtGrabber.hpp"
#include "database/Artist.hpp" #include "database/Artist.hpp"
#include "database/Cluster.hpp" #include "database/Cluster.hpp"
@@ -56,7 +58,6 @@
#include "PlayQueue.hpp" #include "PlayQueue.hpp"
#include "SettingsView.hpp" #include "SettingsView.hpp"
namespace UserInterface { namespace UserInterface {
static constexpr const char* defaultPath {"/releases"}; static constexpr const char* defaultPath {"/releases"};
@@ -82,16 +83,16 @@ LmsApplication::getDbSession()
Wt::Dbo::ptr<Database::User> Wt::Dbo::ptr<Database::User>
LmsApplication::getUser() LmsApplication::getUser()
{ {
if (!_userId) if (!_authenticatedUser)
return {}; return {};
return Database::User::getById(getDbSession(), *_userId); return Database::User::getById(getDbSession(), _authenticatedUser->userId);
} }
bool bool
LmsApplication::isUserAuthStrong() const LmsApplication::isUserAuthStrong() const
{ {
return *_userAuthStrong; return _authenticatedUser->strongAuth;
} }
bool bool
@@ -125,7 +126,27 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
_db {db}, _db {db},
_appGroups {appGroups} _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"); useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
// Add a resource bundle // Add a resource bundle
@@ -160,80 +181,82 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
// Handle Media Scanner events and other session events // Handle Media Scanner events and other session events
enableUpdates(true); enableUpdates(true);
if (Service<::Auth::IEnvService>::exists())
processEnvAuth();
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
LmsApplication::processPasswordAuth()
{
{
std::optional<Database::IdType> userId {processAuthToken(environment())};
if (userId)
{
LMS_LOG(UI, DEBUG) << "User authenticated using Auth token!";
_authenticatedUser = {*userId, false};
onUserLoggedIn();
return;
}
}
setTheme();
// If here is no account in the database, launch the first connection wizard // If here is no account in the database, launch the first connection wizard
bool firstConnection {}; bool firstConnection {};
{ {
auto transaction {getDbSession().createSharedTransaction()}; auto transaction {getDbSession().createSharedTransaction()};
firstConnection = Database::User::getAll(getDbSession()).empty(); firstConnection = Database::User::getCount(getDbSession()) == 0;
} }
LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection; LMS_LOG(UI, DEBUG) << "Creating root widget. First connection = " << firstConnection;
if (firstConnection) if (firstConnection && Service<::Auth::IPasswordService>::get()->canSetPasswords())
{ {
setTheme(std::make_unique<LmsTheme>(Database::User::defaultUITheme));
root()->addWidget(std::make_unique<InitWizardView>()); root()->addWidget(std::make_unique<InitWizardView>());
return;
}
const auto userId {processAuthToken(env)};
{
Database::User::UITheme theme {Database::User::defaultUITheme};
if (userId)
{
auto transaction {getDbSession().createSharedTransaction()};
const auto user {Database::User::getById(getDbSession(), *userId)};
if (user)
theme = user->getUITheme();
}
setTheme(std::make_unique<LmsTheme>(theme));
}
if (userId)
{
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
}
} }
else else
{ {
Auth* auth {root()->addNew<Auth>()}; Auth* auth {root()->addNew<Auth>()};
auth->userLoggedIn.connect(this, [this](Database::IdType userId) auth->userLoggedIn.connect(this, [this](Database::IdType userId)
{ {
{ _authenticatedUser = {userId, true};
auto transaction {getDbSession().createSharedTransaction()}; onUserLoggedIn();
const auto user {Database::User::getById(getDbSession(), userId)};
if (user)
{
LmsTheme* lmsTheme {static_cast<LmsTheme*>(LmsApp->theme().get())};
lmsTheme->setTheme(user->getUITheme());
}
}
handleUserLoggedIn(userId, true);
}); });
} }
} }
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<LmsTheme>(theme));
}
void void
LmsApplication::finalize() LmsApplication::finalize()
{ {
if (_userId) if (_authenticatedUser)
{ {
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment()); LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
@@ -403,28 +426,25 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
LmsApplicationGroup& LmsApplicationGroup&
LmsApplication::getApplicationGroup() LmsApplication::getApplicationGroup()
{ {
return _appGroups.get(*_userId); return _appGroups.get(_authenticatedUser->userId);
} }
void void
LmsApplication::handleUserLoggedOut() LmsApplication::logoutUser()
{ {
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out";
{ {
auto transaction {getDbSession().createUniqueTransaction()}; auto transaction {getDbSession().createUniqueTransaction()};
getUser().modify()->clearAuthTokens(); getUser().modify()->clearAuthTokens();
} }
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out";
goHomeAndQuit(); goHomeAndQuit();
} }
void void
LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth) LmsApplication::onUserLoggedIn()
{ {
_userId = userId; setTheme();
_userAuthStrong = strongAuth;
root()->clear(); root()->clear();
const LmsApplicationInfo info {LmsApplicationInfo::fromEnvironment(environment())}; const LmsApplicationInfo info {LmsApplicationInfo::fromEnvironment(environment())};
@@ -466,9 +486,9 @@ LmsApplication::createHome()
main->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings")); main->bindNew<Wt::WAnchor>("settings", Wt::WLink {Wt::LinkType::InternalPath, "/settings"}, Wt::WString::tr("Lms.Settings.menu-settings"));
{ {
auto* logout {main->bindNew<Wt::WAnchor>("logout")}; Wt::WAnchor* logout {main->bindNew<Wt::WAnchor>("logout")};
logout->setText(Wt::WString::tr("Lms.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<Wt::WLineEdit>("search")}; Wt::WLineEdit* searchEdit {main->bindNew<Wt::WLineEdit>("search")};
@@ -483,10 +503,10 @@ LmsApplication::createHome()
// Contents // Contents
// Order is important in mainStack, see IdxRoot! // Order is important in mainStack, see IdxRoot!
Wt::WStackedWidget* mainStack = main->bindNew<Wt::WStackedWidget>("contents"); Wt::WStackedWidget* mainStack {main->bindNew<Wt::WStackedWidget>("contents")};
mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;"); mainStack->setAttributeValue("style", "overflow-x:visible;overflow-y:visible;");
Explore* explore = mainStack->addNew<Explore>(filters); Explore* explore {mainStack->addNew<Explore>(filters)};
_playQueue = mainStack->addNew<PlayQueue>(); _playQueue = mainStack->addNew<PlayQueue>();
mainStack->addNew<SettingsView>(); mainStack->addNew<SettingsView>();
+18 -10
View File
@@ -27,11 +27,6 @@
#include "LmsApplicationGroup.hpp" #include "LmsApplicationGroup.hpp"
namespace Wt
{
class WPopupMenu;
}
namespace Database namespace Database
{ {
class Artist; class Artist;
@@ -41,10 +36,13 @@ namespace Database
class Session; class Session;
class User; class User;
} }
namespace Wt
{
class WPopupMenu;
}
namespace UserInterface { namespace UserInterface {
class Auth;
class CoverResource; class CoverResource;
class LmsApplicationException; class LmsApplicationException;
class MediaPlayer; class MediaPlayer;
@@ -61,12 +59,14 @@ struct Events
class LmsApplication : public Wt::WApplication class LmsApplication : public Wt::WApplication
{ {
public: public:
LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups); LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups);
~LmsApplication(); ~LmsApplication();
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups); static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups);
static LmsApplication* instance(); static LmsApplication* instance();
// Session application data // Session application data
std::shared_ptr<CoverResource> getCoverResource() { return _coverResource; } std::shared_ptr<CoverResource> getCoverResource() { return _coverResource; }
Database::Session& getDbSession(); // always thread safe Database::Session& getDbSession(); // always thread safe
@@ -108,14 +108,18 @@ class LmsApplication : public Wt::WApplication
private: private:
void init();
void setTheme();
void processEnvAuth();
void processPasswordAuth();
void handleException(LmsApplicationException& e); void handleException(LmsApplicationException& e);
void goHomeAndQuit(); void goHomeAndQuit();
LmsApplicationGroup& getApplicationGroup(); LmsApplicationGroup& getApplicationGroup();
// Signal slots // Signal slots
void handleUserLoggedOut(); void logoutUser();
void handleUserLoggedIn(Database::IdType userId, bool strongAuth); void onUserLoggedIn();
void notify(const Wt::WEvent& event) override; void notify(const Wt::WEvent& event) override;
void finalize() override; void finalize() override;
@@ -127,8 +131,12 @@ class LmsApplication : public Wt::WApplication
LmsApplicationGroupContainer& _appGroups; LmsApplicationGroupContainer& _appGroups;
Events _events; Events _events;
Scanner::Events _scannerEvents; Scanner::Events _scannerEvents;
std::optional<Database::IdType> _userId; struct UserAuthInfo
std::optional<bool> _userAuthStrong; {
Database::IdType userId;
bool strongAuth {};
};
std::optional<UserAuthInfo> _authenticatedUser;
std::shared_ptr<CoverResource> _coverResource; std::shared_ptr<CoverResource> _coverResource;
MediaPlayer* _mediaPlayer {}; MediaPlayer* _mediaPlayer {};
PlayQueue* _playQueue {}; PlayQueue* _playQueue {};
+6
View File
@@ -31,6 +31,12 @@ class LmsApplicationException : public LmsException
LmsApplicationException(const Wt::WString& error) : LmsException {error.toUTF8()} {} 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 class ArtistNotFoundException : public LmsApplicationException
{ {
public: public:
+31 -46
View File
@@ -29,7 +29,8 @@
#include <Wt/WString.h> #include <Wt/WString.h>
#include <Wt/WTemplateFormView.h> #include <Wt/WTemplateFormView.h>
#include "common/Validators.hpp" #include "common/PasswordValidator.hpp"
#include "common/MandatoryValidator.hpp"
#include "common/ValueStringModel.hpp" #include "common/ValueStringModel.hpp"
#include "auth/IPasswordService.hpp" #include "auth/IPasswordService.hpp"
@@ -68,8 +69,9 @@ class SettingsModel : public Wt::WFormModel
using TranscodeModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>; using TranscodeModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>;
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>; using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
SettingsModel(bool withOldPassword) SettingsModel(::Auth::IPasswordService* authPasswordService, bool withOldPassword)
: _withOldPassword {withOldPassword} : _authPasswordService {authPasswordService}
, _withOldPassword {withOldPassword}
{ {
initializeModels(); initializeModels();
@@ -84,11 +86,18 @@ class SettingsModel : public Wt::WFormModel
addField(SubsonicTranscodeBitrateField); addField(SubsonicTranscodeBitrateField);
addField(SubsonicTranscodeFormatField); addField(SubsonicTranscodeFormatField);
if (_authPasswordService)
{
if (_withOldPassword) if (_withOldPassword)
{
addField(PasswordOldField); addField(PasswordOldField);
setValidator(PasswordOldField, createPasswordCheckValidator());
}
addField(PasswordField); addField(PasswordField);
setValidator(PasswordField, createPasswordStrengthValidator(LmsApp->getUserLoginName()));
addField(PasswordConfirmField); addField(PasswordConfirmField);
}
setValidator(TranscodeModeField, createMandatoryValidator()); setValidator(TranscodeModeField, createMandatoryValidator());
setValidator(TranscodeBitrateField, createMandatoryValidator()); setValidator(TranscodeBitrateField, createMandatoryValidator());
@@ -118,11 +127,6 @@ class SettingsModel : public Wt::WFormModel
void saveData() void saveData()
{ {
User::PasswordHash passwordHash;
if (!valueText(PasswordField).empty())
passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
User::pointer user {LmsApp->getUser()}; User::pointer user {LmsApp->getUser()};
@@ -172,15 +176,15 @@ class SettingsModel : public Wt::WFormModel
user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow)); user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow));
} }
if (!valueText(PasswordField).empty())
{
user.modify()->setPasswordHash(passwordHash);
user.modify()->clearAuthTokens();
}
auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))}; auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))};
if (subsonicArtistListModeRow) if (subsonicArtistListModeRow)
user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow)); user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow));
if (_authPasswordService && !valueText(PasswordField).empty())
{
_authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
}
} }
void loadData() void loadData()
@@ -242,47 +246,18 @@ class SettingsModel : public Wt::WFormModel
if (field == PasswordOldField) if (field == PasswordOldField)
{ {
if (!valueText(PasswordOldField).empty()) if (valueText(PasswordOldField).empty() && !valueText(PasswordField).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;
}
}
else
{
if (!valueText(PasswordField).empty())
error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password"); error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password");
else else
return Wt::WFormModel::validateField(field); return Wt::WFormModel::validateField(field);
} }
}
else if (field == PasswordField) else if (field == PasswordField)
{ {
if (!valueText(PasswordField).empty()) if (!valueText(PasswordOldField).empty() && valueText(PasswordField).empty())
{
if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8()))
error = Wt::WString::tr("Lms.password-too-weak");
}
else
{
if (!valueText(PasswordOldField).empty())
error = Wt::WString::tr("Wt.WValidator.Invalid"); error = Wt::WString::tr("Wt.WValidator.Invalid");
else else
return Wt::WFormModel::validateField(field); return Wt::WFormModel::validateField(field);
} }
}
else if (field == PasswordConfirmField) else if (field == PasswordConfirmField)
{ {
if (validation(PasswordField).state() == Wt::ValidationState::Valid) if (validation(PasswordField).state() == Wt::ValidationState::Valid)
@@ -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); _subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), User::SubsonicArtistListMode::TrackArtists);
} }
::Auth::IPasswordService* _authPasswordService {};
bool _withOldPassword {}; bool _withOldPassword {};
std::shared_ptr<TranscodeModeModel> _transcodeModeModel; std::shared_ptr<TranscodeModeModel> _transcodeModeModel;
@@ -374,7 +350,11 @@ SettingsView::refreshView()
auto t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template"))}; auto t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template"))};
auto model {std::make_shared<SettingsModel>(!LmsApp->isUserAuthStrong())}; auto* authPasswordService {Service<::Auth::IPasswordService>::get()};
if (authPasswordService && !authPasswordService->canSetPasswords())
authPasswordService = nullptr;
auto model {std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong())};
// Appearance // Appearance
{ {
@@ -382,6 +362,10 @@ SettingsView::refreshView()
t->setFormWidget(SettingsModel::DarkModeField, std::move(darkMode)); t->setFormWidget(SettingsModel::DarkModeField, std::move(darkMode));
} }
if (authPasswordService)
{
t->setCondition("if-has-change-password", true);
// Old password // Old password
if (!LmsApp->isUserAuthStrong()) if (!LmsApp->isUserAuthStrong())
{ {
@@ -404,6 +388,7 @@ SettingsView::refreshView()
passwordConfirm->setEchoMode(Wt::EchoMode::Password); passwordConfirm->setEchoMode(Wt::EchoMode::Password);
passwordConfirm->setAttributeValue("autocomplete", "new-password"); passwordConfirm->setAttributeValue("autocomplete", "new-password");
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
}
// Audio // Audio
{ {
+3 -2
View File
@@ -34,7 +34,8 @@
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
#include "common/Validators.hpp" #include "common/DirectoryValidator.hpp"
#include "common/MandatoryValidator.hpp"
#include "common/ValueStringModel.hpp" #include "common/ValueStringModel.hpp"
#include "ScannerController.hpp" #include "ScannerController.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -66,7 +67,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
addField(RecommendationEngineTypeField); addField(RecommendationEngineTypeField);
addField(TagsField); addField(TagsField);
auto dirValidator {std::make_shared<DirectoryValidator>()}; auto dirValidator {createDirectoryValidator()};
dirValidator->setMandatory(true); dirValidator->setMandatory(true);
setValidator(MediaDirectoryField, dirValidator); setValidator(MediaDirectoryField, dirValidator);
+21 -89
View File
@@ -31,8 +31,9 @@
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "common/Validators.hpp" #include "common/LoginNameValidator.hpp"
#include "common/AuthModeModel.hpp" #include "common/MandatoryValidator.hpp"
#include "common/PasswordValidator.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
namespace UserInterface { namespace UserInterface {
@@ -42,118 +43,63 @@ class InitWizardModel : public Wt::WFormModel
public: public:
// Associate each field with a unique string literal. // Associate each field with a unique string literal.
static const Field AdminLoginField; static inline const Field AdminLoginField {"admin-login"};
static const Field PasswordField; static inline const Field PasswordField {"password"};
static const Field PasswordConfirmField; static inline const Field PasswordConfirmField {"password-confirm"};
static inline const Field AuthModeField{"auth-mode"};
InitWizardModel() : Wt::WFormModel() InitWizardModel() : Wt::WFormModel()
{ {
addField(AdminLoginField); addField(AdminLoginField);
addField(AuthModeField);
addField(PasswordField); addField(PasswordField);
addField(PasswordConfirmField); addField(PasswordConfirmField);
setValidator(AuthModeField, createMandatoryValidator()); setValidator(AdminLoginField, createLoginNameValidator());
setValidator(AdminLoginField, createNameValidator()); setValidator(PasswordField, createPasswordStrengthValidator([this] { return valueText(AdminLoginField).toUTF8(); }));
setValidator(PasswordField, createMandatoryValidator()); validator(PasswordField)->setMandatory(true);
setValidator(PasswordConfirmField, createMandatoryValidator()); setValidator(PasswordConfirmField, createMandatoryValidator());
} }
std::shared_ptr<AuthModeModel> getAuthModeModel() const { return _authModeModel; }
void saveData() void saveData()
{ {
const Database::User::PasswordHash passwordHash {Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8())};
auto transaction(LmsApp->getDbSession().createUniqueTransaction()); auto transaction(LmsApp->getDbSession().createUniqueTransaction());
// Check if a user already exist // Check if a user already exist
// If it's the case, just do nothing // If it's the case, just do nothing
if (!Database::User::getAll(LmsApp->getDbSession()).empty()) if (!Database::User::getAll(LmsApp->getDbSession()).empty())
throw LmsException("Admin user already created"); 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)};
Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())}; Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())};
user.modify()->setType(Database::User::Type::ADMIN); user.modify()->setType(Database::User::Type::ADMIN);
user.modify()->setAuthMode(authMode); Service<::Auth::IPasswordService>::get()->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
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");
}
} }
bool validateField(Field field) bool validateField(Field field)
{ {
Wt::WString error; Wt::WString error;
if (field == PasswordField) if (field == PasswordConfirmField)
{ {
validatePassword(error); if (validation(PasswordField).state() == Wt::ValidationState::Valid
&& valueText(PasswordField) != valueText(PasswordConfirmField))
{
error = Wt::WString::tr("Lms.passwords-dont-match");
} }
else if (field == PasswordConfirmField) else
{ return Wt::WFormModel::validateField(field);
validatePasswordConfirm(error);
} }
else else
{ {
return Wt::WFormModel::validateField(field); 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> _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() 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<InitWizardModel>(); auto model = std::make_shared<InitWizardModel>();
@@ -164,20 +110,6 @@ InitWizardView::InitWizardView()
setFormWidget(InitWizardModel::AdminLoginField, std::move(adminLogin)); setFormWidget(InitWizardModel::AdminLoginField, std::move(adminLogin));
} }
// Auth mode
auto authMode = std::make_unique<Wt::WComboBox>();
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 // Password
{ {
auto passwordEdit = std::make_unique<Wt::WLineEdit>(); auto passwordEdit = std::make_unique<Wt::WLineEdit>();
+43 -106
View File
@@ -36,9 +36,8 @@
#include "utils/Service.hpp" #include "utils/Service.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
#include "common/AuthModeModel.hpp" #include "common/LoginNameValidator.hpp"
#include "common/Validators.hpp" #include "common/PasswordValidator.hpp"
#include "common/ValueStringModel.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "LmsApplicationException.hpp" #include "LmsApplicationException.hpp"
@@ -53,122 +52,62 @@ class UserModel : public Wt::WFormModel
static inline const Field LoginField {"login"}; static inline const Field LoginField {"login"};
static inline const Field PasswordField {"password"}; static inline const Field PasswordField {"password"};
static inline const Field DemoField {"demo"}; static inline const Field DemoField {"demo"};
static inline const Field AuthModeField{"auth-mode"};
using AuthModeModel = ValueStringModel<User::AuthMode>; UserModel(std::optional<Database::IdType> userId, ::Auth::IPasswordService* authPasswordService)
UserModel(std::optional<Database::IdType> userId)
: _userId {userId} : _userId {userId}
, _authPasswordService {authPasswordService}
{ {
if (!_userId) if (!_userId)
{ {
addField(LoginField); addField(LoginField);
setValidator(LoginField, createNameValidator()); setValidator(LoginField, createLoginNameValidator());
} }
addField(AuthModeField); if (authPasswordService)
{
addField(PasswordField); addField(PasswordField);
setValidator(PasswordField, createPasswordStrengthValidator([this] { return getLoginName(); }));
if (!userId)
validator(PasswordField)->setMandatory(true);
}
addField(DemoField); addField(DemoField);
setValidator(AuthModeField, createMandatoryValidator());
loadData(); loadData();
} }
std::shared_ptr<AuthModeModel> getAuthModeModel() const { return _authModeModel; }
void saveData() void saveData()
{ {
std::optional<Database::User::PasswordHash> passwordHash;
if (!valueText(PasswordField).empty())
passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
if (_userId) if (_userId)
{ {
// Update user // Update user
Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)}; Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)};
if (!user)
throw UserNotFoundException {*_userId};
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; if (_authPasswordService && !valueText(PasswordField).empty())
if (!authModeRow) _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
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();
}
} }
else 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 // 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))) if (Wt::asNumber(value(DemoField)))
user.modify()->setType(Database::User::Type::DEMO); user.modify()->setType(Database::User::Type::DEMO);
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))}; if (_authPasswordService)
if (!authModeRow) _authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
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);
} }
} }
private: 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() void loadData()
{ {
if (!_userId) if (!_userId)
@@ -181,14 +120,6 @@ class UserModel : public Wt::WFormModel
throw UserNotFoundException {*_userId}; throw UserNotFoundException {*_userId};
else if (user == LmsApp->getUser()) else if (user == LmsApp->getUser())
throw UserNotAllowedException {}; 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 std::string getLoginName() const
@@ -204,6 +135,16 @@ class UserModel : public Wt::WFormModel
return valueText(LoginField).toUTF8(); 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) bool validateField(Field field)
{ {
Wt::WString error; Wt::WString error;
@@ -237,7 +178,7 @@ class UserModel : public Wt::WFormModel
} }
std::optional<Database::IdType> _userId; std::optional<Database::IdType> _userId;
std::shared_ptr<AuthModeModel> _authModeModel {createAuthModeModel()}; ::Auth::IPasswordService* _authPasswordService {};
}; };
UserView::UserView() UserView::UserView()
@@ -262,7 +203,11 @@ UserView::refreshView()
Wt::WTemplateFormView* t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.User.template"))}; Wt::WTemplateFormView* t {addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.User.template"))};
auto model {std::make_shared<UserModel>(userId)}; auto* authPasswordService {Service<::Auth::IPasswordService>::get()};
if (authPasswordService && !authPasswordService->canSetPasswords())
authPasswordService = nullptr;
auto model {std::make_shared<UserModel>(userId, authPasswordService)};
if (userId) if (userId)
{ {
@@ -284,31 +229,23 @@ UserView::refreshView()
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create")); t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create"));
} }
// Auth mode if (authPasswordService)
auto authMode = std::make_unique<Wt::WComboBox>();
authMode->setModel(model->getAuthModeModel());
authMode->activated().connect([=](int row)
{ {
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 // Password
auto passwordEdit = std::make_unique<Wt::WLineEdit>(); auto passwordEdit = std::make_unique<Wt::WLineEdit>();
passwordEdit->setEchoMode(Wt::EchoMode::Password); passwordEdit->setEchoMode(Wt::EchoMode::Password);
passwordEdit->setAttributeValue("autocomplete", "off"); passwordEdit->setAttributeValue("autocomplete", "off");
t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit)); t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit));
}
// Demo account // Demo account
t->setFormWidget(UserModel::DemoField, std::make_unique<Wt::WCheckBox>()); t->setFormWidget(UserModel::DemoField, std::make_unique<Wt::WCheckBox>());
if (!userId && Service<IConfig>::get()->getBool("demo", false)) if (!userId && Service<IConfig>::get()->getBool("demo", false))
t->setCondition("if-demo", true); t->setCondition("if-demo", true);
Wt::WPushButton* saveBtn = t->bindNew<Wt::WPushButton>("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create")); Wt::WPushButton* saveBtn {t->bindNew<Wt::WPushButton>("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create"))};
saveBtn->clicked().connect([=]() saveBtn->clicked().connect([=]()
{ {
t->updateModel(model.get()); t->updateModel(model.get());
+7
View File
@@ -23,9 +23,11 @@
#include <Wt/WMessageBox.h> #include <Wt/WMessageBox.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "auth/IPasswordService.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Service.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -38,11 +40,16 @@ UsersView::UsersView()
_container = bindNew<Wt::WContainerWidget>("users"); _container = bindNew<Wt::WContainerWidget>("users");
if (Service<::Auth::IPasswordService>::get() && Service<::Auth::IPasswordService>::get()->canSetPasswords())
{
setCondition("if-can-create-user", true);
Wt::WPushButton* addBtn = bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Admin.Users.add")); Wt::WPushButton* addBtn = bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Admin.Users.add"));
addBtn->clicked().connect([]() addBtn->clicked().connect([]()
{ {
LmsApp->setInternalPath("/admin/user", true); LmsApp->setInternalPath("/admin/user", true);
}); });
}
wApp->internalPathChanged().connect(this, [this]() wApp->internalPathChanged().connect(this, [this]()
{ {
+56
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "DirectoryValidator.hpp"
#include <filesystem>
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<Wt::WValidator>
createDirectoryValidator()
{
return std::make_unique<DirectoryValidator>();
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2020 Emeric Poupon * Copyright (C) 2021 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,14 +19,10 @@
#pragma once #pragma once
#ifdef LMS_SUPPORT_PAM #include <Wt/WValidator.h>
#include <string> namespace UserInterface
namespace Auth::PAM
{ {
bool checkUserPassword(const std::string& loginName, const std::string& password); std::shared_ptr<Wt::WValidator> createDirectoryValidator();
} } // namespace UserInterface
#endif // LMS_SUPPORT_PAM
@@ -17,25 +17,20 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#pragma once #include "LoginNameValidator.hpp"
#include <Wt/WValidator.h>
#include <Wt/WLengthValidator.h>
#include "database/User.hpp" #include "database/User.hpp"
namespace UserInterface { namespace UserInterface
std::shared_ptr<Wt::WValidator> createNameValidator();
std::shared_ptr<Wt::WValidator> createMandatoryValidator();
class DirectoryValidator : public Wt::WValidator
{ {
public: std::shared_ptr<Wt::WValidator>
DirectoryValidator(); createLoginNameValidator()
{
Wt::WValidator::Result validate(const Wt::WString& input) const override; auto v = std::make_unique<Wt::WLengthValidator>();
v->setMandatory(true);
}; v->setMinimumLength(::Database::User::MinNameLength);
v->setMaximumLength(::Database::User::MaxNameLength);
return v;
}
} // namespace UserInterface } // namespace UserInterface
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2020 Emeric Poupon * Copyright (C) 2013 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,13 +19,10 @@
#pragma once #pragma once
#include "database/User.hpp" #include <Wt/WValidator.h>
#include "common/ValueStringModel.hpp"
namespace UserInterface namespace UserInterface
{ {
using AuthModeModel = ValueStringModel<Database::User::AuthMode>; std::shared_ptr<Wt::WValidator> createLoginNameValidator();
} // namespace UserInterface
std::unique_ptr<AuthModeModel> createAuthModeModel();
}
+31
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "MandatoryValidator.hpp"
namespace UserInterface
{
std::shared_ptr<Wt::WValidator>
createMandatoryValidator()
{
auto v {std::make_shared<Wt::WValidator>()};
v->setMandatory(true);
return v;
}
} // namespace UserInterface
+28
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/WValidator.h>
namespace UserInterface
{
std::shared_ptr<Wt::WValidator> createMandatoryValidator();
} // namespace UserInterface
+96
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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<Wt::WValidator>
createPasswordStrengthValidator(std::string_view loginName)
{
return std::make_shared<PasswordStrengthValidator>([loginName = std::string {loginName}] { return loginName; });
}
std::shared_ptr<Wt::WValidator> createPasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc)
{
return std::make_shared<PasswordStrengthValidator>(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<Wt::WValidator>
createPasswordCheckValidator()
{
return std::make_shared<PasswordCheckValidator>();
}
} // namespace UserInterface
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2020 Emeric Poupon * Copyright (C) 2021 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -17,26 +17,17 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "AuthModeModel.hpp" #pragma once
#include "auth/IPasswordService.hpp" #include <Wt/WValidator.h>
#include "utils/Service.hpp"
namespace UserInterface namespace UserInterface
{ {
std::shared_ptr<Wt::WValidator> createPasswordStrengthValidator(std::string_view loginName);
using LoginNameGetFunc = std::function<std::string()>;
std::shared_ptr<Wt::WValidator> createPasswordStrengthValidator(LoginNameGetFunc loginNameGetFunc);
std::unique_ptr<AuthModeModel> // Check current user password
createAuthModeModel() std::shared_ptr<Wt::WValidator> createPasswordCheckValidator();
{ } // namespace UserInterface
auto model {std::make_unique<AuthModeModel>()};
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;
}
}
-70
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include "Validators.hpp"
#include <filesystem>
#include <Wt/WLengthValidator.h>
namespace UserInterface {
std::shared_ptr<Wt::WValidator>
createNameValidator()
{
auto v = std::make_shared<Wt::WLengthValidator>();
v->setMandatory(true);
v->setMinimumLength(::Database::User::MinNameLength);
v->setMaximumLength(::Database::User::MaxNameLength);
return v;
}
std::shared_ptr<Wt::WValidator>
createMandatoryValidator()
{
auto v = std::make_shared<Wt::WValidator>();
//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
+1
View File
@@ -1,4 +1,5 @@
add_subdirectory(database) add_subdirectory(database)
add_subdirectory(som) add_subdirectory(som)
add_subdirectory(utils)
+12
View File
@@ -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)
+121
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <atomic>
#include <cassert>
#include <chrono>
#include <iostream>
#include <thread>
#include <vector>
#include <stdlib.h>
#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<std::thread> threads;
RecursiveSharedMutex mutex;
std::atomic<std::size_t> nbUnique {};
std::atomic<std::size_t> 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;
}