Added authentication backends: internal, pam and http-headers. fixes #119
This commit is contained in:
@@ -8,6 +8,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
include(CTest)
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(Filesystem REQUIRED)
|
||||
find_package(FFMPEGAV REQUIRED)
|
||||
find_package(Boost REQUIRED COMPONENTS system program_options)
|
||||
|
||||
+11
-1
@@ -8,6 +8,7 @@
|
||||
+ [Upgrade](#upgrade)
|
||||
- [Deployment](#deployment)
|
||||
* [Configuration](#configuration)
|
||||
* [Authentication backend](#authentication-backend)
|
||||
* [Deploy on non root path](#deploy-on-non-root-path)
|
||||
* [Reverse proxy settings](#reverse-proxy-settings)
|
||||
- [Run](#run)
|
||||
@@ -125,7 +126,16 @@ _LMS_ uses a configuration file, installed by default in `/etc/lms.conf`. It is
|
||||
|
||||
All other settings are set using the web interface (user management, scan settings, transcode settings, ...).
|
||||
|
||||
If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default.conf](conf/lms.conf) file)
|
||||
If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default configuration file](conf/lms.conf))
|
||||
|
||||
## Authentication backend
|
||||
|
||||
You can define which authentication backend to be used thanks to the `authentication-backend` option:
|
||||
* `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users.
|
||||
* `PAM`: the authentication request is forwarded to PAM (see the [default configuration file](conf/pam/lms)).
|
||||
* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-user-field` option.
|
||||
|
||||
__Note__: the first created user is the admin user
|
||||
|
||||
## Deploy on non root path
|
||||
If you want to deploy on non root path (e.g. https://mydomain.com/newroot/), you have to set the `deploy-path` option accordingly in `lms.conf`.
|
||||
|
||||
@@ -18,7 +18,6 @@ A [demo instance](http://lms.demo.poupon.io) is available. Note the administrati
|
||||
* Persistent play queue across sessions
|
||||
* _Systemd_ integration
|
||||
* User management
|
||||
* with optional _PAM_ authentication backend
|
||||
* Subsonic API, with the following additional features:
|
||||
* Playlists
|
||||
* Bookmarks
|
||||
@@ -30,7 +29,7 @@ _LMS_ provides several ways to help you find the music you like:
|
||||
* Radio mode, based on what is in the current playqueue
|
||||
* Searches in album, artist and track names (including sort names)
|
||||
* Starred Albums/Artists/Tracks
|
||||
* Custom tags support to help you filter your music: _mood_, _albummood_, _albumgenre_, _albumgrouping_, ...
|
||||
* Various tags to help you filter your music: _mood_, _albummood_, _albumgenre_, _albumgrouping_, ...
|
||||
* Random/Starred/Most played/Recently played/Recently added for Artist/Albums/Tracks, allowing you to search for things like:
|
||||
* Recently added _Electronic_ artists
|
||||
* Random _Metal_ and _Aggressive_ albums
|
||||
@@ -54,7 +53,7 @@ Since _LMS_ uses metadata tags to organize music, a compatibility mode is used t
|
||||
|
||||
The Subsonic API is enabled by default.
|
||||
|
||||
__Note__: since _LMS_ stores hashed and salted passwords, it cannot handle the __token authentication__ method defined from version 1.13.0.
|
||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method defined from version 1.13.0.
|
||||
|
||||
## Keyboard shortcuts
|
||||
* Play/pause: <kbd>Space</bbd>
|
||||
|
||||
@@ -58,9 +58,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${apply-btn class="btn-primary"} ${discard-btn}
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${apply-btn class="btn-primary"} ${discard-btn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -20,15 +20,6 @@
|
||||
${admin-login-info class="help-block"}
|
||||
</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">
|
||||
<label class="col-lg-3 control-label" for="${id:password}">
|
||||
${tr:Lms.password}
|
||||
|
||||
@@ -30,15 +30,7 @@
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-login>}
|
||||
<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>
|
||||
${<if-has-password>}
|
||||
<div class="form-group">
|
||||
<label class="control-label col-lg-3" for="${id:password}">
|
||||
${tr:Lms.password-new}
|
||||
@@ -48,6 +40,7 @@
|
||||
${password-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-password>}
|
||||
${<if-demo>}
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-3" for="${id:demo}">
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<h2>${tr:Lms.Admin.Users.users}</h2>
|
||||
</div>
|
||||
${users}
|
||||
${<if-can-create-user>}
|
||||
${add-btn class="btn-primary Lms-admin-users-add-btn"}
|
||||
${</if-can-create-user>}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Admin.Users.template.entry">
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<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-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-old">Old password</message>
|
||||
<message id="Lms.password-too-weak">Password too weak</message>
|
||||
@@ -29,6 +28,7 @@
|
||||
<!--Errors-->
|
||||
<message id="Lms.Error.artist-not-found">Artist not found</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.release-not-found">Release not found</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>
|
||||
|
||||
<!--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-already-exists">Demo account already exists!</message>
|
||||
<message id="Lms.Admin.User.demo-password-invalid">Demo password must be the login name!</message>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<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-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-old">Ancien mot de passe</message>
|
||||
<message id="Lms.password-too-weak">Mot de passe trop faible</message>
|
||||
@@ -29,6 +28,7 @@
|
||||
<!--Errors-->
|
||||
<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.deployment-error">Erreur de déploiement</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.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>
|
||||
|
||||
<!--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-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>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<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-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-old">Vecchia password</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>
|
||||
|
||||
<!--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-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>
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-subsonic-api>}
|
||||
${<if-has-change-password>}
|
||||
<legend>${tr:Lms.Settings.change-password}</legend>
|
||||
<div class="form-horizontal">
|
||||
${<if-has-old-password>}
|
||||
@@ -162,7 +163,9 @@
|
||||
${password-confirm-info class="help-block"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
${</if-has-change-password>}
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-3 col-lg-9">
|
||||
${apply-btn class="btn-primary"} ${discard-btn}
|
||||
|
||||
+8
-3
@@ -37,15 +37,20 @@ http-server-thread-count = 0;
|
||||
# Acoustic brainz's root API
|
||||
acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/";
|
||||
|
||||
# Authentication
|
||||
# Available backends: "internal", "PAM", "http-headers"
|
||||
authentication-backend = "internal";
|
||||
http-headers-user-field = "X-Forwarded-User";
|
||||
|
||||
# Max entries in the login throttler (1 entry per IP address. For IPv6, the whole /64 block is used)
|
||||
login-throttler-max-entries = 10000;
|
||||
|
||||
# API
|
||||
api-subsonic = true;
|
||||
|
||||
# Turn on this option to allow the demo account creation/use
|
||||
demo = false;
|
||||
|
||||
# Max entries in the login throttler (1 entry per client)
|
||||
login-throttler-max-entries = 10000;
|
||||
|
||||
# Max external cover file size in MBytes
|
||||
cover-max-file-size = 10;
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
|
||||
add_library(lmsauth SHARED
|
||||
impl/AuthTokenService.cpp
|
||||
impl/PasswordService.cpp
|
||||
impl/AuthServiceBase.cpp
|
||||
impl/EnvService.cpp
|
||||
impl/LoginThrottler.cpp
|
||||
impl/PasswordServiceBase.cpp
|
||||
impl/http-headers/HttpHeadersEnvService.cpp
|
||||
impl/internal/InternalPasswordService.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsauth INTERFACE
|
||||
@@ -11,6 +15,7 @@ target_include_directories(lmsauth INTERFACE
|
||||
|
||||
target_include_directories(lmsauth PRIVATE
|
||||
include
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmsauth PRIVATE
|
||||
@@ -26,7 +31,7 @@ target_link_libraries(lmsauth PUBLIC
|
||||
|
||||
if (USE_PAM)
|
||||
target_compile_options(lmsauth PRIVATE "-DLMS_SUPPORT_PAM")
|
||||
target_sources(lmsauth PRIVATE impl/pam/PAM.cpp)
|
||||
target_sources(lmsauth PRIVATE impl/pam/PAMPasswordService.cpp)
|
||||
target_include_directories(lmsauth PRIVATE ${PAM_INCLUDE_DIR})
|
||||
target_link_libraries(lmsauth PRIVATE ${PAM_LIBRARIES})
|
||||
endif (USE_PAM)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,103 +23,115 @@
|
||||
#include <Wt/Auth/PasswordStrengthValidator.h>
|
||||
#include <Wt/WRandom.h>
|
||||
|
||||
#include "auth/Types.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Auth {
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntries)
|
||||
namespace Auth
|
||||
{
|
||||
return std::make_unique<AuthTokenService>(maxThrottlerEntries);
|
||||
}
|
||||
|
||||
static const Wt::Auth::SHA1HashFunction sha1Function;
|
||||
|
||||
AuthTokenService::AuthTokenService(std::size_t maxThrottlerEntries)
|
||||
: _loginThrottler {maxThrottlerEntries}
|
||||
{
|
||||
}
|
||||
|
||||
std::string
|
||||
AuthTokenService::createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry)
|
||||
{
|
||||
const std::string secret {Wt::WRandom::generateId(32)};
|
||||
const std::string secretHash {sha1Function.compute(secret, {})};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
if (!user)
|
||||
throw LmsException {"User deleted"};
|
||||
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, expiry, user)};
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
|
||||
|
||||
if (user->getAuthTokensCount() >= 50)
|
||||
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
|
||||
processAuthToken(Database::Session& session, const std::string& secret)
|
||||
{
|
||||
const std::string secretHash {sha1Function.compute(secret, {})};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)};
|
||||
if (!authToken)
|
||||
return std::nullopt;
|
||||
|
||||
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntries)
|
||||
{
|
||||
authToken.remove();
|
||||
return std::nullopt;
|
||||
return std::make_unique<AuthTokenService>(maxThrottlerEntries);
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
|
||||
static const Wt::Auth::SHA1HashFunction sha1Function;
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser().id(), authToken->getExpiry()};
|
||||
authToken.remove();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult
|
||||
AuthTokenService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue)
|
||||
{
|
||||
// Do not waste too much resource on brute force attacks (optim)
|
||||
AuthTokenService::AuthTokenService(std::size_t maxThrottlerEntries)
|
||||
: _loginThrottler {maxThrottlerEntries}
|
||||
{
|
||||
std::shared_lock<std::shared_timed_mutex> lock {_mutex};
|
||||
|
||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
|
||||
}
|
||||
|
||||
auto res {Auth::processAuthToken(session, tokenValue)};
|
||||
std::string
|
||||
AuthTokenService::createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry)
|
||||
{
|
||||
std::unique_lock<std::shared_timed_mutex> lock {_mutex};
|
||||
const std::string secret {Wt::WRandom::generateId(32)};
|
||||
const std::string secretHash {sha1Function.compute(secret, {})};
|
||||
|
||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
if (!res)
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
if (!user)
|
||||
throw Exception {"User deleted"};
|
||||
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::create(session, secretHash, expiry, user)};
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
|
||||
|
||||
if (user->getAuthTokensCount() >= 50)
|
||||
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
|
||||
processAuthToken(Database::Session& session, std::string_view secret)
|
||||
{
|
||||
const std::string secretHash {sha1Function.compute(std::string {secret}, {})};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)};
|
||||
if (!authToken)
|
||||
return std::nullopt;
|
||||
|
||||
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
|
||||
{
|
||||
_loginThrottler.onBadClientAttempt(clientAddress);
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::NotFound};
|
||||
authToken.remove();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Found, std::move(*res)};
|
||||
}
|
||||
}
|
||||
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser().id(), authToken->getExpiry()};
|
||||
authToken.remove();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult
|
||||
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)
|
||||
{
|
||||
std::shared_lock lock {_mutex};
|
||||
|
||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
|
||||
}
|
||||
|
||||
auto res {Auth::processAuthToken(session, tokenValue)};
|
||||
{
|
||||
std::unique_lock lock {_mutex};
|
||||
|
||||
if (_loginThrottler.isClientThrottled(clientAddress))
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
|
||||
|
||||
if (!res)
|
||||
{
|
||||
_loginThrottler.onBadClientAttempt(clientAddress);
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Denied};
|
||||
}
|
||||
|
||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||
onUserAuthenticated(session, res->userId);
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Granted, std::move(*res)};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
AuthTokenService::clearAuthTokens(Database::Session& session, Database::IdType userId)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
if (!user)
|
||||
throw Exception {"User deleted"};
|
||||
|
||||
user.modify()->clearAuthTokens();
|
||||
}
|
||||
|
||||
} // namespace Auth
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
#include <shared_mutex>
|
||||
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
#include "AuthServiceBase.hpp"
|
||||
#include "LoginThrottler.hpp"
|
||||
|
||||
namespace Database
|
||||
@@ -28,13 +30,11 @@ namespace Database
|
||||
class Session;
|
||||
}
|
||||
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class AuthTokenService : public IAuthTokenService
|
||||
namespace Auth
|
||||
{
|
||||
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
|
||||
{
|
||||
public:
|
||||
|
||||
AuthTokenService(std::size_t maxThrottlerEntries);
|
||||
|
||||
AuthTokenService(const AuthTokenService&) = delete;
|
||||
@@ -42,14 +42,12 @@ namespace Auth {
|
||||
AuthTokenService(AuthTokenService&&) = delete;
|
||||
AuthTokenService& operator=(AuthTokenService&&) = delete;
|
||||
|
||||
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) override;
|
||||
std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) override;
|
||||
|
||||
private:
|
||||
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
|
||||
std::string createAuthToken(Database::Session& session, Database::IdType userId, const Wt::WDateTime& expiry) override;
|
||||
void clearAuthTokens(Database::Session& session, Database::IdType userId) override;
|
||||
|
||||
std::shared_timed_mutex _mutex;
|
||||
LoginThrottler _loginThrottler;
|
||||
std::shared_mutex _mutex;
|
||||
LoginThrottler _loginThrottler;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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!"};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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 <string>
|
||||
#include <string_view>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
@@ -31,11 +32,11 @@
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class User;
|
||||
}
|
||||
|
||||
|
||||
namespace Auth {
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
class IAuthTokenService
|
||||
{
|
||||
public:
|
||||
@@ -46,9 +47,9 @@ namespace Auth {
|
||||
{
|
||||
enum class State
|
||||
{
|
||||
Found,
|
||||
Granted,
|
||||
Throttled,
|
||||
NotFound,
|
||||
Denied,
|
||||
};
|
||||
|
||||
struct AuthTokenInfo
|
||||
@@ -57,16 +58,17 @@ namespace Auth {
|
||||
Wt::WDateTime expiry;
|
||||
};
|
||||
|
||||
State state {State::NotFound};
|
||||
State state {State::Denied};
|
||||
std::optional<AuthTokenInfo> authTokenInfo {};
|
||||
};
|
||||
|
||||
// Removed if found
|
||||
virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& tokenValue) = 0;
|
||||
virtual std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) = 0;
|
||||
// Provided token is only accepted once
|
||||
virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
|
||||
|
||||
// Returns a one time token
|
||||
virtual std::string createAuthToken(Database::Session& session, Database::IdType userid, const Wt::WDateTime& expiry) = 0;
|
||||
virtual void clearAuthTokens(Database::Session& session, Database::IdType userid) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<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
|
||||
@@ -19,44 +19,59 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
|
||||
#include "database/User.hpp"
|
||||
#include "auth/Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
|
||||
namespace Auth {
|
||||
class IAuthTokenService;
|
||||
|
||||
class IPasswordService
|
||||
{
|
||||
public:
|
||||
|
||||
virtual ~IPasswordService() = default;
|
||||
|
||||
// Password services
|
||||
enum class PasswordCheckResult
|
||||
struct CheckResult
|
||||
{
|
||||
Match,
|
||||
Mismatch,
|
||||
Throttled,
|
||||
enum class State
|
||||
{
|
||||
Granted,
|
||||
Denied,
|
||||
Throttled,
|
||||
};
|
||||
State state {State::Denied};
|
||||
std::optional<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;
|
||||
|
||||
class PasswordTooWeakException : public Auth::Exception
|
||||
{
|
||||
public:
|
||||
PasswordTooWeakException() : Auth::Exception {"Password too weak"} {}
|
||||
};
|
||||
|
||||
virtual bool isAuthModeSupported(Database::User::AuthMode authMode) const = 0;
|
||||
|
||||
virtual PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) = 0;
|
||||
virtual Database::User::PasswordHash hashPassword(const std::string& password) const = 0;
|
||||
virtual bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const = 0;
|
||||
virtual bool canSetPasswords() const = 0;
|
||||
virtual bool isPasswordSecureEnough(std::string_view username, std::string_view password) const = 0;
|
||||
virtual void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::size_t maxThrottlerEntryCount);
|
||||
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"} {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ target_include_directories(lmscover INTERFACE
|
||||
|
||||
target_include_directories(lmscover PRIVATE
|
||||
include
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmscover PRIVATE
|
||||
@@ -39,7 +40,5 @@ else ()
|
||||
message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided")
|
||||
endif()
|
||||
|
||||
target_include_directories(lmscover PRIVATE impl)
|
||||
|
||||
install(TARGETS lmscover DESTINATION lib)
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <string_view>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
@@ -40,17 +41,16 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 28
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION {29};
|
||||
|
||||
using Version = std::size_t;
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<VersionInfo>;
|
||||
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<VersionInfo>;
|
||||
|
||||
static VersionInfo::pointer getOrCreate(Session& session)
|
||||
{
|
||||
static VersionInfo::pointer getOrCreate(Session& session)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
pointer versionInfo {session.getDboSession().find<VersionInfo>()};
|
||||
@@ -270,7 +270,7 @@ CREATE TABLE "user_backup" (
|
||||
else if (version == 24)
|
||||
{
|
||||
// User's AuthMode
|
||||
_session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<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)
|
||||
{
|
||||
@@ -290,6 +290,31 @@ CREATE TABLE "user_backup" (
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 28)
|
||||
{
|
||||
// Drop Auth mode
|
||||
_session.execute(R"(
|
||||
CREATE TABLE "user_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"type" integer not null,
|
||||
"login_name" text not null,
|
||||
"password_salt" text not null,
|
||||
"password_hash" text not null,
|
||||
"last_login" text,
|
||||
"subsonic_transcode_enable" boolean not null,
|
||||
"subsonic_transcode_format" integer not null,
|
||||
"subsonic_transcode_bitrate" integer not null,
|
||||
"subsonic_artist_list_mode" integer not null,
|
||||
"ui_theme" integer not null,
|
||||
"cur_playing_track_pos" integer not null,
|
||||
"repeat_all" boolean not null,
|
||||
"radio" boolean not null
|
||||
))");
|
||||
_session.execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
|
||||
_session.execute("DROP TABLE user");
|
||||
_session.execute("ALTER TABLE user_backup RENAME TO user");
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
@@ -329,46 +354,28 @@ enum class OwnedLock
|
||||
Unique,
|
||||
};
|
||||
|
||||
static thread_local std::map<std::shared_mutex*, OwnedLock> lockDebug;
|
||||
|
||||
UniqueTransaction::UniqueTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session)
|
||||
UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::None);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::Unique;
|
||||
}
|
||||
|
||||
UniqueTransaction::~UniqueTransaction()
|
||||
{
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::Unique);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::None;
|
||||
}
|
||||
|
||||
SharedTransaction::SharedTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session)
|
||||
SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::None);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::Shared;
|
||||
}
|
||||
|
||||
SharedTransaction::~SharedTransaction()
|
||||
{
|
||||
assert(lockDebug[_lock.mutex()] == OwnedLock::Shared);
|
||||
lockDebug[_lock.mutex()] = OwnedLock::None;
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkUniqueLocked()
|
||||
{
|
||||
assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique);
|
||||
// assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique);
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkSharedLocked()
|
||||
{
|
||||
assert(lockDebug[&_db.getMutex()] != OwnedLock::None);
|
||||
// assert(lockDebug[&_db.getMutex()] != OwnedLock::None);
|
||||
}
|
||||
|
||||
UniqueTransaction
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -70,7 +71,7 @@ AuthToken::getByValue(Session& session, const std::string& value)
|
||||
static const std::string playedListName {"__played_tracks__"};
|
||||
static const std::string queuedListName {"__queued_tracks__"};
|
||||
|
||||
User::User(const std::string& loginName)
|
||||
User::User(std::string_view loginName)
|
||||
: _loginName {loginName}
|
||||
{
|
||||
}
|
||||
@@ -93,8 +94,16 @@ User::getDemo(Session& session)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
User::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::create(Session& session, const std::string& loginName)
|
||||
User::create(Session& session, std::string_view loginName)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -115,7 +124,7 @@ User::getById(Session& session, IdType id)
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::getByLoginName(Session& session, const std::string& name)
|
||||
User::getByLoginName(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().find<User>()
|
||||
.where("login_name = ?").bind(name);
|
||||
|
||||
@@ -20,10 +20,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <shared_mutex>
|
||||
|
||||
#include <Wt/Dbo/SqlConnectionPool.h>
|
||||
|
||||
#include "utils/RecursiveSharedMutex.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Session;
|
||||
@@ -44,7 +45,7 @@ class Db
|
||||
private:
|
||||
friend class Session;
|
||||
|
||||
std::shared_mutex& getMutex() { return _sharedMutex; }
|
||||
RecursiveSharedMutex& getMutex() { return _sharedMutex; }
|
||||
Wt::Dbo::SqlConnectionPool& getConnectionPool() { return *_connectionPool; }
|
||||
|
||||
class ScopedConnection
|
||||
@@ -88,7 +89,7 @@ class Db
|
||||
|
||||
void executeSql(const std::string& sql);
|
||||
|
||||
std::shared_mutex _sharedMutex;
|
||||
RecursiveSharedMutex _sharedMutex;
|
||||
std::unique_ptr<Wt::Dbo::SqlConnectionPool> _connectionPool;
|
||||
|
||||
std::mutex _tlsSessionsMutex;
|
||||
|
||||
@@ -19,74 +19,68 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <shared_mutex>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/Dbo/SqlConnectionPool.h>
|
||||
|
||||
namespace Database {
|
||||
#include "utils/RecursiveSharedMutex.hpp"
|
||||
|
||||
class UniqueTransaction
|
||||
namespace Database
|
||||
{
|
||||
public:
|
||||
~UniqueTransaction();
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
UniqueTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session);
|
||||
class UniqueTransaction
|
||||
{
|
||||
private:
|
||||
friend class Session;
|
||||
UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
|
||||
|
||||
std::unique_lock<std::shared_mutex> _lock;
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
std::unique_lock<RecursiveSharedMutex> _lock;
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
|
||||
class SharedTransaction
|
||||
{
|
||||
public:
|
||||
~SharedTransaction();
|
||||
class SharedTransaction
|
||||
{
|
||||
private:
|
||||
friend class Session;
|
||||
SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
SharedTransaction(std::shared_mutex& mutex, Wt::Dbo::Session& session);
|
||||
std::shared_lock<RecursiveSharedMutex> _lock;
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
|
||||
std::shared_lock<std::shared_mutex> _lock;
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
class Db;
|
||||
class Session
|
||||
{
|
||||
public:
|
||||
Session (Db& database);
|
||||
|
||||
class Db;
|
||||
class Session
|
||||
{
|
||||
public:
|
||||
Session (Db& database);
|
||||
Session(const Session&) = delete;
|
||||
Session(Session&&) = delete;
|
||||
Session& operator=(const Session&) = delete;
|
||||
Session& operator=(Session&&) = delete;
|
||||
|
||||
Session(const Session&) = delete;
|
||||
Session(Session&&) = delete;
|
||||
Session& operator=(const Session&) = delete;
|
||||
Session& operator=(Session&&) = delete;
|
||||
[[nodiscard]] UniqueTransaction createUniqueTransaction();
|
||||
[[nodiscard]] SharedTransaction createSharedTransaction();
|
||||
|
||||
[[nodiscard]] UniqueTransaction createUniqueTransaction();
|
||||
[[nodiscard]] SharedTransaction createSharedTransaction();
|
||||
void checkUniqueLocked();
|
||||
void checkSharedLocked();
|
||||
|
||||
void checkUniqueLocked();
|
||||
void checkSharedLocked();
|
||||
void optimize();
|
||||
|
||||
void optimize();
|
||||
void prepareTables(); // need to run only once at startup
|
||||
|
||||
void prepareTables(); // need to run only once at startup
|
||||
Wt::Dbo::Session& getDboSession() { return _session; }
|
||||
|
||||
Wt::Dbo::Session& getDboSession() { return _session; }
|
||||
private:
|
||||
Session(std::shared_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool);
|
||||
|
||||
private:
|
||||
Session(std::shared_mutex& mutex, Wt::Dbo::SqlConnectionPool& connectionPool);
|
||||
void doDatabaseMigrationIfNeeded();
|
||||
|
||||
void doDatabaseMigrationIfNeeded();
|
||||
|
||||
Db& _db;
|
||||
Wt::Dbo::Session _session;
|
||||
};
|
||||
Db& _db;
|
||||
Wt::Dbo::Session _session;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
@@ -100,12 +101,6 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
DEMO = 2,
|
||||
};
|
||||
|
||||
enum class AuthMode
|
||||
{
|
||||
Internal = 0,
|
||||
PAM = 1,
|
||||
};
|
||||
|
||||
struct PasswordHash
|
||||
{
|
||||
std::string salt;
|
||||
@@ -144,19 +139,19 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
static inline const Bitrate defaultSubsonicTranscodeBitrate {128000};
|
||||
static inline const UITheme defaultUITheme {UITheme::Dark};
|
||||
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists};
|
||||
static inline const AuthMode defaultAuthMode {AuthMode::Internal};
|
||||
|
||||
|
||||
User() = default;
|
||||
User(const std::string& loginName);
|
||||
User(std::string_view loginName);
|
||||
|
||||
// utility
|
||||
static pointer create(Session& session, const std::string& loginName);
|
||||
static pointer create(Session& session, std::string_view loginName);
|
||||
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getByLoginName(Session& session, const std::string& loginName);
|
||||
static pointer getByLoginName(Session& session, std::string_view loginName);
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static pointer getDemo(Session& session);
|
||||
static std::size_t getCount(Session& session);
|
||||
|
||||
// accessors
|
||||
const std::string& getLoginName() const { return _loginName; }
|
||||
@@ -173,7 +168,6 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
void setSubsonicTranscodeBitrate(Bitrate bitrate);
|
||||
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
|
||||
void setRadio(bool val) { _radio = val; }
|
||||
void setAuthMode(AuthMode mode) { _authMode = mode;}
|
||||
void setRepeatAll(bool val) { _repeatAll = val; }
|
||||
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
||||
void clearAuthTokens();
|
||||
@@ -188,7 +182,6 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
|
||||
bool isRepeatAllSet() const { return _repeatAll; }
|
||||
bool isRadioSet() const { return _radio; }
|
||||
AuthMode getAuthMode() const { return _authMode; }
|
||||
UITheme getUITheme() const { return _uiTheme; }
|
||||
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
|
||||
|
||||
@@ -225,7 +218,6 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
|
||||
Wt::Dbo::field(a, _repeatAll, "repeat_all");
|
||||
Wt::Dbo::field(a, _radio, "radio");
|
||||
Wt::Dbo::field(a, _authMode, "auth_mode");
|
||||
Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user");
|
||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -248,13 +240,12 @@ class User : public Wt::Dbo::Dbo<User>
|
||||
SubsonicArtistListMode _subsonicArtistListMode {defaultSubsonicArtistListMode};
|
||||
bool _subsonicTranscodeEnable {defaultSubsonicTranscodeEnable};
|
||||
AudioFormat _subsonicTranscodeFormat {defaultSubsonicTranscodeFormat};
|
||||
int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate};
|
||||
int _subsonicTranscodeBitrate {defaultSubsonicTranscodeBitrate};
|
||||
|
||||
// User's dynamic data (UI)
|
||||
int _curPlayingTrackPos {}; // Current track position in queue
|
||||
bool _repeatAll {};
|
||||
bool _radio {};
|
||||
AuthMode _authMode {defaultAuthMode};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackList>> _tracklists;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _starredArtists;
|
||||
|
||||
@@ -38,9 +38,9 @@ static
|
||||
std::string
|
||||
getJsonData(const UUID& mbid)
|
||||
{
|
||||
static const std::string defaultAPIURL = "https://acousticbrainz.org/api/v1/";
|
||||
static constexpr std::string_view defaultAPIURL {"https://acousticbrainz.org/api/v1/"};
|
||||
|
||||
const std::string url {Service<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;
|
||||
|
||||
|
||||
@@ -105,6 +105,15 @@ namespace StringUtils
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
static
|
||||
void
|
||||
checkSetPasswordImplemented()
|
||||
{
|
||||
Auth::IPasswordService* passwordService {Service<Auth::IPasswordService>::get()};
|
||||
if (!passwordService || !passwordService->canSetPasswords())
|
||||
throw NotImplementedGenericError {};
|
||||
}
|
||||
|
||||
static
|
||||
std::string
|
||||
makeNameFilesystemCompatible(const std::string& name)
|
||||
@@ -515,21 +524,31 @@ handleChangePassword(RequestContext& context)
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
|
||||
|
||||
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password))
|
||||
try
|
||||
{
|
||||
Database::IdType userId;
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
User::pointer user {User::getByLoginName(context.dbSession, username)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
userId = user.id();
|
||||
}
|
||||
|
||||
Service<Auth::IPasswordService>::get()->setPassword(context.dbSession, userId, password);
|
||||
}
|
||||
catch (Auth::IPasswordService::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError {};
|
||||
|
||||
const User::PasswordHash hash {Service<Auth::IPasswordService>::get()->hashPassword(password)};
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
User::pointer user {User::getByLoginName(context.dbSession, username)};
|
||||
if (!user)
|
||||
}
|
||||
catch (Auth::Exception& authException)
|
||||
{
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
user.modify()->setPasswordHash(hash);
|
||||
user.modify()->clearAuthTokens();
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context);
|
||||
}
|
||||
@@ -597,19 +616,40 @@ handleCreateUserRequest(RequestContext& context)
|
||||
std::string password {decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password"))};
|
||||
// Just ignore all the other fields as we don't handle them
|
||||
|
||||
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, password))
|
||||
Database::IdType userId;
|
||||
{
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
User::pointer user {User::getByLoginName(context.dbSession, username)};
|
||||
if (user)
|
||||
throw UserAlreadyExistsGenericError {};
|
||||
|
||||
user = User::create(context.dbSession, username);
|
||||
userId = user.id();
|
||||
}
|
||||
|
||||
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 {};
|
||||
|
||||
const User::PasswordHash hash {Service<Auth::IPasswordService>::get()->hashPassword(password)};
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
if (User::getByLoginName(context.dbSession, username) != User::pointer{})
|
||||
throw UserAlreadyExistsGenericError {};
|
||||
|
||||
User::pointer user {User::create(context.dbSession, username)};
|
||||
user.modify()->setAuthMode(User::AuthMode::Internal);
|
||||
user.modify()->setPasswordHash(hash);
|
||||
}
|
||||
catch (const Auth::Exception& exception)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw UserNotAuthorizedError {};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context);
|
||||
}
|
||||
@@ -1614,26 +1654,33 @@ handleUpdateUserRequest(RequestContext& context)
|
||||
std::string username {getMandatoryParameterAs<std::string>(context.parameters, "username")};
|
||||
std::optional<std::string> password {getParameterAs<std::string>(context.parameters, "password")};
|
||||
|
||||
User::PasswordHash hash;
|
||||
if (password)
|
||||
Database::IdType userId;
|
||||
{
|
||||
*password = decodePasswordIfNeeded(*password);
|
||||
if (!Service<Auth::IPasswordService>::get()->evaluatePasswordStrength(username, *password))
|
||||
throw PasswordTooWeakGenericError {};
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
hash = Service<Auth::IPasswordService>::get()->hashPassword(*password);
|
||||
User::pointer user {User::getByLoginName(context.dbSession, username)};
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
userId = user.id();
|
||||
}
|
||||
|
||||
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||
|
||||
User::pointer user {User::getByLoginName(context.dbSession, username)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
if (password)
|
||||
{
|
||||
user.modify()->setPasswordHash(hash);
|
||||
user.modify()->clearAuthTokens();
|
||||
checkSetPasswordImplemented();
|
||||
|
||||
try
|
||||
{
|
||||
Service<::Auth::IPasswordService>()->setPassword(context.dbSession, userId, decodePasswordIfNeeded(*password));
|
||||
}
|
||||
catch (const Auth::IPasswordService::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError {};
|
||||
}
|
||||
catch (const Auth::Exception&)
|
||||
{
|
||||
throw UserNotAuthorizedError {};
|
||||
}
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context);
|
||||
@@ -1826,10 +1873,12 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/,
|
||||
}
|
||||
|
||||
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
|
||||
using CheckImplementedFunc = std::function<void()>;
|
||||
struct RequestEntryPointInfo
|
||||
{
|
||||
RequestHandlerFunc func;
|
||||
bool mustBeAdmin;
|
||||
RequestHandlerFunc func;
|
||||
bool mustBeAdmin;
|
||||
CheckImplementedFunc checkFunc {};
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
||||
@@ -1918,12 +1967,12 @@ static std::unordered_map<std::string, RequestEntryPointInfo> requestEntryPoints
|
||||
{"addChatMessages", {handleNotImplemented, false}},
|
||||
|
||||
// User management
|
||||
{"getUser", {handleGetUserRequest, false}},
|
||||
{"getUser", {handleGetUserRequest, false}},
|
||||
{"getUsers", {handleGetUsersRequest, true}},
|
||||
{"createUser", {handleCreateUserRequest, true}},
|
||||
{"createUser", {handleCreateUserRequest, true, &checkSetPasswordImplemented}},
|
||||
{"updateUser", {handleUpdateUserRequest, true}},
|
||||
{"deleteUser", {handleDeleteUserRequest, true}},
|
||||
{"changePassword", {handleChangePassword, false}},
|
||||
{"changePassword", {handleChangePassword, false, &checkSetPasswordImplemented}},
|
||||
|
||||
// Bookmarks
|
||||
{"getBookmarks", {handleGetBookmarks, false}},
|
||||
@@ -1975,15 +2024,17 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
|
||||
|
||||
Session& dbSession {_db.getTLSSession()};
|
||||
|
||||
switch (Service<Auth::IPasswordService>::get()->checkUserPassword(dbSession,
|
||||
boost::asio::ip::address::from_string(request.clientAddress()),
|
||||
clientInfo.user, clientInfo.password))
|
||||
const Auth::IPasswordService::CheckResult checkResult {Service<Auth::IPasswordService>::get()->checkUserPassword(dbSession,
|
||||
boost::asio::ip::address::from_string(request.clientAddress()),
|
||||
clientInfo.user, clientInfo.password)};
|
||||
|
||||
switch (checkResult.state)
|
||||
{
|
||||
case Auth::IPasswordService::PasswordCheckResult::Match:
|
||||
case Auth::IPasswordService::CheckResult::State::Granted:
|
||||
break;
|
||||
case Auth::IPasswordService::PasswordCheckResult::Mismatch:
|
||||
case Auth::IPasswordService::CheckResult::State::Denied:
|
||||
throw WrongUsernameOrPasswordError {};
|
||||
case Auth::IPasswordService::PasswordCheckResult::Throttled:
|
||||
case Auth::IPasswordService::CheckResult::State::Throttled:
|
||||
throw LoginThrottledGenericError {};
|
||||
}
|
||||
|
||||
@@ -1992,6 +2043,9 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp
|
||||
auto itEntryPoint {requestEntryPoints.find(requestPath)};
|
||||
if (itEntryPoint != requestEntryPoints.end())
|
||||
{
|
||||
if (itEntryPoint->second.checkFunc)
|
||||
itEntryPoint->second.checkFunc();
|
||||
|
||||
if (itEntryPoint->second.mustBeAdmin)
|
||||
{
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
@@ -8,6 +8,7 @@ add_library(lmsutils SHARED
|
||||
impl/NetAddress.cpp
|
||||
impl/Path.cpp
|
||||
impl/Random.cpp
|
||||
impl/RecursiveSharedMutex.cpp
|
||||
impl/StreamLogger.cpp
|
||||
impl/String.cpp
|
||||
impl/UUID.cpp
|
||||
|
||||
@@ -47,70 +47,62 @@ Config::Config(const std::filesystem::path& p)
|
||||
}
|
||||
}
|
||||
|
||||
std::string
|
||||
Config::getString(const std::string& setting, const std::string& def, const std::unordered_set<std::string>& allowedValues)
|
||||
std::string_view
|
||||
Config::getString(std::string_view setting, std::string_view def)
|
||||
{
|
||||
try {
|
||||
std::string res {(const char*)_config.lookup(setting)};
|
||||
|
||||
if (!allowedValues.empty() && allowedValues.find(res) == std::cend(allowedValues))
|
||||
{
|
||||
LMS_LOG(MAIN, ERROR) << "Invalid setting for '" << setting << "', using default value '" << def << "'";
|
||||
return def;
|
||||
}
|
||||
|
||||
return res;
|
||||
return static_cast<const char*>(_config.lookup(std::string {setting}));
|
||||
}
|
||||
catch (std::exception &e)
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path
|
||||
Config::getPath(const std::string& setting, const std::filesystem::path& path)
|
||||
Config::getPath(std::string_view setting, const std::filesystem::path& path)
|
||||
{
|
||||
try {
|
||||
const char* res = _config.lookup(setting);
|
||||
const char* res {_config.lookup(std::string {setting})};
|
||||
return std::filesystem::path {std::string(res)};
|
||||
}
|
||||
catch (std::exception &e)
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long
|
||||
Config::getULong(const std::string& setting, unsigned long def)
|
||||
Config::getULong(std::string_view setting, unsigned long def)
|
||||
{
|
||||
try {
|
||||
return static_cast<unsigned int>(_config.lookup(setting));
|
||||
return static_cast<unsigned int>(_config.lookup(std::string {setting}));
|
||||
}
|
||||
catch (...)
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
long
|
||||
Config::getLong(const std::string& setting, long def)
|
||||
Config::getLong(std::string_view setting, long def)
|
||||
{
|
||||
try {
|
||||
return _config.lookup(setting);
|
||||
return _config.lookup(std::string {setting});
|
||||
}
|
||||
catch (...)
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
Config::getBool(const std::string& setting, bool def)
|
||||
Config::getBool(std::string_view setting, bool def)
|
||||
{
|
||||
try {
|
||||
return _config.lookup(setting);
|
||||
return _config.lookup(std::string {setting});
|
||||
}
|
||||
catch (...)
|
||||
catch (libconfig::ConfigException&)
|
||||
{
|
||||
return def;
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ class Config final : public IConfig
|
||||
Config& operator=(Config&&) = delete;
|
||||
|
||||
// Default values are returned in case of setting not found
|
||||
std::string getString(const std::string& setting, const std::string& def = "", const std::unordered_set<std::string>& allowedValues = {}) override;
|
||||
std::filesystem::path getPath(const std::string& setting, const std::filesystem::path& def = std::filesystem::path()) override;
|
||||
unsigned long getULong(const std::string& setting, unsigned long def = 0) override;
|
||||
long getLong(const std::string& setting, long def = 0) override;
|
||||
bool getBool(const std::string& setting, bool def = false) override;
|
||||
std::string_view getString(std::string_view setting, std::string_view def = "") override;
|
||||
std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) override;
|
||||
unsigned long getULong(std::string_view setting, unsigned long def = 0) override;
|
||||
long getLong(std::string_view setting, long def = 0) override;
|
||||
bool getBool(std::string_view setting, bool def = false) override;
|
||||
|
||||
private:
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -18,9 +18,8 @@
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
|
||||
// Used to get config values from configuration files
|
||||
class IConfig
|
||||
@@ -30,11 +29,11 @@ class IConfig
|
||||
virtual ~IConfig() = default;
|
||||
|
||||
// Default values are returned in case of setting not found
|
||||
virtual std::string getString(const std::string& setting, const std::string& def = "", const std::unordered_set<std::string>& allowedValues = {}) = 0;
|
||||
virtual std::filesystem::path getPath(const std::string& setting, const std::filesystem::path& def = std::filesystem::path()) = 0;
|
||||
virtual unsigned long getULong(const std::string& setting, unsigned long def = 0) = 0;
|
||||
virtual long getLong(const std::string& setting, long def = 0) = 0;
|
||||
virtual bool getBool(const std::string& setting, bool def = false) = 0;
|
||||
virtual std::string_view getString(std::string_view setting, std::string_view def = "") = 0;
|
||||
virtual std::filesystem::path getPath(std::string_view setting, const std::filesystem::path& def = std::filesystem::path()) = 0;
|
||||
virtual unsigned long getULong(std::string_view setting, unsigned long def = 0) = 0;
|
||||
virtual long getLong(std::string_view setting, long def = 0) = 0;
|
||||
virtual bool getBool(std::string_view setting, bool def = false) = 0;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ template <typename Class>
|
||||
class Service
|
||||
{
|
||||
public:
|
||||
Service() = default;
|
||||
Service(std::unique_ptr<Class> service)
|
||||
{
|
||||
assign(std::move(service));
|
||||
@@ -52,14 +53,17 @@ class Service
|
||||
}
|
||||
|
||||
static Class* get() { return _service.get(); }
|
||||
static bool exists() { return _service.get(); }
|
||||
|
||||
private:
|
||||
static Class& assign(std::unique_ptr<Class> service)
|
||||
template <typename SubClass>
|
||||
static Class& assign(std::unique_ptr<SubClass> service)
|
||||
{
|
||||
assert(!_service);
|
||||
_service = std::move(service);
|
||||
return *get();
|
||||
}
|
||||
|
||||
private:
|
||||
static void clear() { _service.reset(); }
|
||||
|
||||
static inline std::unique_ptr<Class> _service;
|
||||
|
||||
@@ -14,9 +14,11 @@ add_executable(lms
|
||||
ui/admin/InitWizardView.cpp
|
||||
ui/admin/UserView.cpp
|
||||
ui/admin/UsersView.cpp
|
||||
ui/common/AuthModeModel.cpp
|
||||
ui/common/DirectoryValidator.cpp
|
||||
ui/common/LoadingIndicator.cpp
|
||||
ui/common/Validators.cpp
|
||||
ui/common/LoginNameValidator.cpp
|
||||
ui/common/MandatoryValidator.cpp
|
||||
ui/common/PasswordValidator.cpp
|
||||
ui/explore/ArtistListHelpers.cpp
|
||||
ui/explore/ArtistView.cpp
|
||||
ui/explore/ArtistsView.cpp
|
||||
|
||||
+29
-11
@@ -26,6 +26,7 @@
|
||||
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
#include "auth/IPasswordService.hpp"
|
||||
#include "auth/IEnvService.hpp"
|
||||
#include "cover/ICoverArtGrabber.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
@@ -36,6 +37,7 @@
|
||||
#include "utils/IChildProcessManager.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "utils/WtLogger.hpp"
|
||||
|
||||
static
|
||||
@@ -52,24 +54,24 @@ generateWtConfig(std::string execPath)
|
||||
|
||||
args.push_back(execPath);
|
||||
args.push_back("--config=" + wtConfigPath.string());
|
||||
args.push_back("--docroot=" + Service<IConfig>::get()->getString("docroot"));
|
||||
args.push_back("--approot=" + Service<IConfig>::get()->getString("approot"));
|
||||
args.push_back("--deploy-path=" + Service<IConfig>::get()->getString("deploy-path", "/"));
|
||||
args.push_back("--docroot=" + std::string {Service<IConfig>::get()->getString("docroot")});
|
||||
args.push_back("--approot=" + std::string {Service<IConfig>::get()->getString("approot")});
|
||||
args.push_back("--deploy-path=" + std::string {Service<IConfig>::get()->getString("deploy-path", "/")});
|
||||
if (!wtResourcesPath.empty())
|
||||
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
||||
|
||||
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-address=" + 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-private-key=" + Service<IConfig>::get()->getString("tls-key"));
|
||||
args.push_back("--ssl-tmp-dh=" + Service<IConfig>::get()->getString("tls-dh"));
|
||||
args.push_back("--https-address=" + std::string {Service<IConfig>::get()->getString("listen-addr", "0.0.0.0")});
|
||||
args.push_back("--ssl-certificate=" + std::string {Service<IConfig>::get()->getString("tls-cert")});
|
||||
args.push_back("--ssl-private-key=" + std::string {Service<IConfig>::get()->getString("tls-key")});
|
||||
args.push_back("--ssl-tmp-dh=" + std::string {Service<IConfig>::get()->getString("tls-dh")});
|
||||
}
|
||||
else
|
||||
{
|
||||
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())
|
||||
@@ -211,10 +213,26 @@ int main(int argc, char* argv[])
|
||||
|
||||
UserInterface::LmsApplicationGroupContainer appGroups;
|
||||
|
||||
// Service initialization order is important
|
||||
// Service initialization order is important (reverse-order for deinit)
|
||||
Service<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],
|
||||
server.appRoot() + "/images/unknown-cover.jpg",
|
||||
config->getULong("cover-max-cache-size", 30) * 1000 * 1000,
|
||||
|
||||
+16
-10
@@ -30,13 +30,17 @@
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
#include "auth/IPasswordService.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "common/LoginNameValidator.hpp"
|
||||
#include "common/MandatoryValidator.hpp"
|
||||
#include "common/PasswordValidator.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
namespace UserInterface
|
||||
{
|
||||
|
||||
static const std::string authCookieName {"LmsAuth"};
|
||||
|
||||
@@ -65,12 +69,12 @@ processAuthToken(const Wt::WEnvironment& env)
|
||||
const auto res {Service<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)};
|
||||
switch (res.state)
|
||||
{
|
||||
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::NotFound:
|
||||
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
|
||||
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
|
||||
LmsApp->setCookie(authCookieName, std::string {}, 0, "", "", env.urlScheme() == "https");
|
||||
return std::nullopt;
|
||||
|
||||
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Found:
|
||||
case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
|
||||
createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry);
|
||||
break;
|
||||
}
|
||||
@@ -93,7 +97,7 @@ class AuthModel : public Wt::WFormModel
|
||||
addField(PasswordField);
|
||||
addField(RememberMeField);
|
||||
|
||||
setValidator(LoginNameField, createNameValidator());
|
||||
setValidator(LoginNameField, createLoginNameValidator());
|
||||
setValidator(PasswordField, createMandatoryValidator());
|
||||
}
|
||||
|
||||
@@ -125,18 +129,20 @@ class AuthModel : public Wt::WFormModel
|
||||
|
||||
if (field == PasswordField)
|
||||
{
|
||||
switch (Service<::Auth::IPasswordService>::get()->checkUserPassword(
|
||||
const auto checkResult {Service<::Auth::IPasswordService>::get()->checkUserPassword(
|
||||
LmsApp->getDbSession(),
|
||||
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
|
||||
valueText(LoginNameField).toUTF8(),
|
||||
valueText(PasswordField).toUTF8()))
|
||||
valueText(PasswordField).toUTF8())};
|
||||
switch (checkResult.state)
|
||||
{
|
||||
case ::Auth::IPasswordService::PasswordCheckResult::Match:
|
||||
case ::Auth::IPasswordService::CheckResult::State::Granted:
|
||||
_userId = *checkResult.userId;
|
||||
break;
|
||||
case ::Auth::IPasswordService::PasswordCheckResult::Mismatch:
|
||||
case ::Auth::IPasswordService::CheckResult::State::Denied:
|
||||
error = Wt::WString::tr("Lms.password-bad-login-combination");
|
||||
break;
|
||||
case ::Auth::IPasswordService::PasswordCheckResult::Throttled:
|
||||
case ::Auth::IPasswordService::CheckResult::State::Throttled:
|
||||
error = Wt::WString::tr("Lms.password-client-throttled");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,4 @@ class Auth : public Wt::WTemplateFormView
|
||||
Wt::Signal<Database::IdType /*userId*/> userLoggedIn;
|
||||
};
|
||||
|
||||
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "auth/IEnvService.hpp"
|
||||
#include "auth/IPasswordService.hpp"
|
||||
#include "cover/ICoverArtGrabber.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
@@ -56,7 +58,6 @@
|
||||
#include "PlayQueue.hpp"
|
||||
#include "SettingsView.hpp"
|
||||
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
static constexpr const char* defaultPath {"/releases"};
|
||||
@@ -82,16 +83,16 @@ LmsApplication::getDbSession()
|
||||
Wt::Dbo::ptr<Database::User>
|
||||
LmsApplication::getUser()
|
||||
{
|
||||
if (!_userId)
|
||||
if (!_authenticatedUser)
|
||||
return {};
|
||||
|
||||
return Database::User::getById(getDbSession(), *_userId);
|
||||
return Database::User::getById(getDbSession(), _authenticatedUser->userId);
|
||||
}
|
||||
|
||||
bool
|
||||
LmsApplication::isUserAuthStrong() const
|
||||
{
|
||||
return *_userAuthStrong;
|
||||
return _authenticatedUser->strongAuth;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -125,7 +126,27 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
_db {db},
|
||||
_appGroups {appGroups}
|
||||
{
|
||||
try
|
||||
{
|
||||
init();
|
||||
}
|
||||
catch (LmsApplicationException& e)
|
||||
{
|
||||
LMS_LOG(UI, WARNING) << "Caught a LmsApplication exception: " << e.what();
|
||||
handleException(e);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
LMS_LOG(UI, ERROR) << "Caught exception: " << e.what();
|
||||
throw LmsException {"Internal error"}; // Do not put details here at it may appear on the user rendered html
|
||||
}
|
||||
}
|
||||
|
||||
LmsApplication::~LmsApplication() = default;
|
||||
|
||||
void
|
||||
LmsApplication::init()
|
||||
{
|
||||
useStyleSheet("resources/font-awesome/css/font-awesome.min.css");
|
||||
|
||||
// Add a resource bundle
|
||||
@@ -160,80 +181,82 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
// Handle Media Scanner events and other session events
|
||||
enableUpdates(true);
|
||||
|
||||
if (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
|
||||
bool firstConnection {};
|
||||
{
|
||||
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;
|
||||
|
||||
if (firstConnection)
|
||||
if (firstConnection && Service<::Auth::IPasswordService>::get()->canSetPasswords())
|
||||
{
|
||||
setTheme(std::make_unique<LmsTheme>(Database::User::defaultUITheme));
|
||||
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
|
||||
{
|
||||
Auth* auth {root()->addNew<Auth>()};
|
||||
auth->userLoggedIn.connect(this, [this](Database::IdType userId)
|
||||
{
|
||||
{
|
||||
auto transaction {getDbSession().createSharedTransaction()};
|
||||
const auto user {Database::User::getById(getDbSession(), userId)};
|
||||
if (user)
|
||||
{
|
||||
LmsTheme* lmsTheme {static_cast<LmsTheme*>(LmsApp->theme().get())};
|
||||
lmsTheme->setTheme(user->getUITheme());
|
||||
}
|
||||
}
|
||||
|
||||
handleUserLoggedIn(userId, true);
|
||||
_authenticatedUser = {userId, true};
|
||||
onUserLoggedIn();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
LmsApplication::~LmsApplication() = default;
|
||||
void
|
||||
LmsApplication::setTheme()
|
||||
{
|
||||
Database::User::UITheme theme {Database::User::defaultUITheme};
|
||||
{
|
||||
auto transaction {getDbSession().createSharedTransaction()};
|
||||
if (const auto user {getUser()})
|
||||
theme = user->getUITheme();
|
||||
}
|
||||
|
||||
WApplication::setTheme(std::make_unique<LmsTheme>(theme));
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::finalize()
|
||||
{
|
||||
if (_userId)
|
||||
if (_authenticatedUser)
|
||||
{
|
||||
LmsApplicationInfo info = LmsApplicationInfo::fromEnvironment(environment());
|
||||
|
||||
@@ -403,28 +426,25 @@ handlePathChange(Wt::WStackedWidget& stack, bool isAdmin)
|
||||
LmsApplicationGroup&
|
||||
LmsApplication::getApplicationGroup()
|
||||
{
|
||||
return _appGroups.get(*_userId);
|
||||
return _appGroups.get(_authenticatedUser->userId);
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::handleUserLoggedOut()
|
||||
LmsApplication::logoutUser()
|
||||
{
|
||||
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out";
|
||||
|
||||
{
|
||||
auto transaction {getDbSession().createUniqueTransaction()};
|
||||
getUser().modify()->clearAuthTokens();
|
||||
}
|
||||
|
||||
LMS_LOG(UI, INFO) << "User '" << getUserLoginName() << " 'logged out";
|
||||
goHomeAndQuit();
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth)
|
||||
LmsApplication::onUserLoggedIn()
|
||||
{
|
||||
_userId = userId;
|
||||
_userAuthStrong = strongAuth;
|
||||
|
||||
setTheme();
|
||||
root()->clear();
|
||||
|
||||
const LmsApplicationInfo info {LmsApplicationInfo::fromEnvironment(environment())};
|
||||
@@ -466,9 +486,9 @@ LmsApplication::createHome()
|
||||
main->bindNew<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->clicked().connect(this, &LmsApplication::handleUserLoggedOut);
|
||||
logout->clicked().connect(this, &LmsApplication::logoutUser);
|
||||
}
|
||||
|
||||
Wt::WLineEdit* searchEdit {main->bindNew<Wt::WLineEdit>("search")};
|
||||
@@ -483,10 +503,10 @@ LmsApplication::createHome()
|
||||
|
||||
// Contents
|
||||
// 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;");
|
||||
|
||||
Explore* explore = mainStack->addNew<Explore>(filters);
|
||||
Explore* explore {mainStack->addNew<Explore>(filters)};
|
||||
_playQueue = mainStack->addNew<PlayQueue>();
|
||||
mainStack->addNew<SettingsView>();
|
||||
|
||||
|
||||
@@ -27,11 +27,6 @@
|
||||
|
||||
#include "LmsApplicationGroup.hpp"
|
||||
|
||||
namespace Wt
|
||||
{
|
||||
class WPopupMenu;
|
||||
}
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
@@ -41,10 +36,13 @@ namespace Database
|
||||
class Session;
|
||||
class User;
|
||||
}
|
||||
namespace Wt
|
||||
{
|
||||
class WPopupMenu;
|
||||
}
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
class Auth;
|
||||
class CoverResource;
|
||||
class LmsApplicationException;
|
||||
class MediaPlayer;
|
||||
@@ -61,12 +59,14 @@ struct Events
|
||||
class LmsApplication : public Wt::WApplication
|
||||
{
|
||||
public:
|
||||
|
||||
LmsApplication(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups);
|
||||
~LmsApplication();
|
||||
|
||||
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, Database::Db& db, LmsApplicationGroupContainer& appGroups);
|
||||
static LmsApplication* instance();
|
||||
|
||||
|
||||
// Session application data
|
||||
std::shared_ptr<CoverResource> getCoverResource() { return _coverResource; }
|
||||
Database::Session& getDbSession(); // always thread safe
|
||||
@@ -108,14 +108,18 @@ class LmsApplication : public Wt::WApplication
|
||||
|
||||
private:
|
||||
|
||||
void init();
|
||||
void setTheme();
|
||||
void processEnvAuth();
|
||||
void processPasswordAuth();
|
||||
void handleException(LmsApplicationException& e);
|
||||
void goHomeAndQuit();
|
||||
|
||||
LmsApplicationGroup& getApplicationGroup();
|
||||
|
||||
// Signal slots
|
||||
void handleUserLoggedOut();
|
||||
void handleUserLoggedIn(Database::IdType userId, bool strongAuth);
|
||||
void logoutUser();
|
||||
void onUserLoggedIn();
|
||||
|
||||
void notify(const Wt::WEvent& event) override;
|
||||
void finalize() override;
|
||||
@@ -127,8 +131,12 @@ class LmsApplication : public Wt::WApplication
|
||||
LmsApplicationGroupContainer& _appGroups;
|
||||
Events _events;
|
||||
Scanner::Events _scannerEvents;
|
||||
std::optional<Database::IdType> _userId;
|
||||
std::optional<bool> _userAuthStrong;
|
||||
struct UserAuthInfo
|
||||
{
|
||||
Database::IdType userId;
|
||||
bool strongAuth {};
|
||||
};
|
||||
std::optional<UserAuthInfo> _authenticatedUser;
|
||||
std::shared_ptr<CoverResource> _coverResource;
|
||||
MediaPlayer* _mediaPlayer {};
|
||||
PlayQueue* _playQueue {};
|
||||
|
||||
@@ -31,6 +31,12 @@ class LmsApplicationException : public LmsException
|
||||
LmsApplicationException(const Wt::WString& error) : LmsException {error.toUTF8()} {}
|
||||
};
|
||||
|
||||
class DeploymentException : public LmsApplicationException
|
||||
{
|
||||
public:
|
||||
DeploymentException() : LmsApplicationException {Wt::WString::tr("Lms.Error.deployment-error")} {}
|
||||
};
|
||||
|
||||
class ArtistNotFoundException : public LmsApplicationException
|
||||
{
|
||||
public:
|
||||
|
||||
+58
-73
@@ -29,7 +29,8 @@
|
||||
#include <Wt/WString.h>
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "common/PasswordValidator.hpp"
|
||||
#include "common/MandatoryValidator.hpp"
|
||||
#include "common/ValueStringModel.hpp"
|
||||
|
||||
#include "auth/IPasswordService.hpp"
|
||||
@@ -68,8 +69,9 @@ class SettingsModel : public Wt::WFormModel
|
||||
using TranscodeModeModel = ValueStringModel<MediaPlayer::Settings::Transcode::Mode>;
|
||||
using ReplayGainModeModel = ValueStringModel<MediaPlayer::Settings::ReplayGain::Mode>;
|
||||
|
||||
SettingsModel(bool withOldPassword)
|
||||
: _withOldPassword {withOldPassword}
|
||||
SettingsModel(::Auth::IPasswordService* authPasswordService, bool withOldPassword)
|
||||
: _authPasswordService {authPasswordService}
|
||||
, _withOldPassword {withOldPassword}
|
||||
{
|
||||
initializeModels();
|
||||
|
||||
@@ -84,11 +86,18 @@ class SettingsModel : public Wt::WFormModel
|
||||
addField(SubsonicTranscodeBitrateField);
|
||||
addField(SubsonicTranscodeFormatField);
|
||||
|
||||
if (_withOldPassword)
|
||||
addField(PasswordOldField);
|
||||
if (_authPasswordService)
|
||||
{
|
||||
if (_withOldPassword)
|
||||
{
|
||||
addField(PasswordOldField);
|
||||
setValidator(PasswordOldField, createPasswordCheckValidator());
|
||||
}
|
||||
|
||||
addField(PasswordField);
|
||||
addField(PasswordConfirmField);
|
||||
addField(PasswordField);
|
||||
setValidator(PasswordField, createPasswordStrengthValidator(LmsApp->getUserLoginName()));
|
||||
addField(PasswordConfirmField);
|
||||
}
|
||||
|
||||
setValidator(TranscodeModeField, createMandatoryValidator());
|
||||
setValidator(TranscodeBitrateField, createMandatoryValidator());
|
||||
@@ -118,11 +127,6 @@ class SettingsModel : public Wt::WFormModel
|
||||
|
||||
void saveData()
|
||||
{
|
||||
User::PasswordHash passwordHash;
|
||||
|
||||
if (!valueText(PasswordField).empty())
|
||||
passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8());
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createUniqueTransaction()};
|
||||
|
||||
User::pointer user {LmsApp->getUser()};
|
||||
@@ -172,15 +176,15 @@ class SettingsModel : public Wt::WFormModel
|
||||
user.modify()->setSubsonicTranscodeFormat(_transcodeFormatModel->getValue(*subsonicTranscodeFormatRow));
|
||||
}
|
||||
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
user.modify()->setPasswordHash(passwordHash);
|
||||
user.modify()->clearAuthTokens();
|
||||
}
|
||||
|
||||
auto subsonicArtistListModeRow {_subsonicArtistListModeModel->getRowFromString(valueText(SubsonicArtistListModeField))};
|
||||
if (subsonicArtistListModeRow)
|
||||
user.modify()->setSubsonicArtistListMode(_subsonicArtistListModeModel->getValue(*subsonicArtistListModeRow));
|
||||
|
||||
if (_authPasswordService && !valueText(PasswordField).empty())
|
||||
{
|
||||
_authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void loadData()
|
||||
@@ -242,46 +246,17 @@ class SettingsModel : public Wt::WFormModel
|
||||
|
||||
if (field == PasswordOldField)
|
||||
{
|
||||
if (!valueText(PasswordOldField).empty())
|
||||
{
|
||||
switch (Service<::Auth::IPasswordService>::get()->checkUserPassword(
|
||||
LmsApp->getDbSession(),
|
||||
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
|
||||
LmsApp->getUserLoginName(),
|
||||
valueText(PasswordOldField).toUTF8()))
|
||||
{
|
||||
case ::Auth::IPasswordService::PasswordCheckResult::Match:
|
||||
break;
|
||||
case ::Auth::IPasswordService::PasswordCheckResult::Mismatch:
|
||||
error = Wt::WString::tr("Lms.Settings.password-bad");
|
||||
break;
|
||||
case ::Auth::IPasswordService::PasswordCheckResult::Throttled:
|
||||
error = Wt::WString::tr("Lms.password-client-throttled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valueText(PasswordOldField).empty() && !valueText(PasswordField).empty())
|
||||
error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password");
|
||||
else
|
||||
{
|
||||
if (!valueText(PasswordField).empty())
|
||||
error = Wt::WString::tr("Lms.Settings.password-must-fill-old-password");
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else if (field == PasswordField)
|
||||
{
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
if (!valueText(PasswordOldField).empty() && valueText(PasswordField).empty())
|
||||
error = Wt::WString::tr("Wt.WValidator.Invalid");
|
||||
else
|
||||
{
|
||||
if (!valueText(PasswordOldField).empty())
|
||||
error = Wt::WString::tr("Wt.WValidator.Invalid");
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else if (field == PasswordConfirmField)
|
||||
{
|
||||
@@ -336,6 +311,7 @@ class SettingsModel : public Wt::WFormModel
|
||||
_subsonicArtistListModeModel->add(Wt::WString::tr("Lms.Settings.subsonic-artist-list-mode.track-artists"), User::SubsonicArtistListMode::TrackArtists);
|
||||
}
|
||||
|
||||
::Auth::IPasswordService* _authPasswordService {};
|
||||
bool _withOldPassword {};
|
||||
|
||||
std::shared_ptr<TranscodeModeModel> _transcodeModeModel;
|
||||
@@ -374,7 +350,11 @@ SettingsView::refreshView()
|
||||
|
||||
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
|
||||
{
|
||||
@@ -382,29 +362,34 @@ SettingsView::refreshView()
|
||||
t->setFormWidget(SettingsModel::DarkModeField, std::move(darkMode));
|
||||
}
|
||||
|
||||
// Old password
|
||||
if (!LmsApp->isUserAuthStrong())
|
||||
if (authPasswordService)
|
||||
{
|
||||
t->setCondition("if-has-old-password", true);
|
||||
t->setCondition("if-has-change-password", true);
|
||||
|
||||
auto oldPassword {std::make_unique<Wt::WLineEdit>()};
|
||||
oldPassword->setEchoMode(Wt::EchoMode::Password);
|
||||
oldPassword->setAttributeValue("autocomplete", "current-password");
|
||||
t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword));
|
||||
// Old password
|
||||
if (!LmsApp->isUserAuthStrong())
|
||||
{
|
||||
t->setCondition("if-has-old-password", true);
|
||||
|
||||
auto oldPassword {std::make_unique<Wt::WLineEdit>()};
|
||||
oldPassword->setEchoMode(Wt::EchoMode::Password);
|
||||
oldPassword->setAttributeValue("autocomplete", "current-password");
|
||||
t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword));
|
||||
}
|
||||
|
||||
// Password
|
||||
auto password {std::make_unique<Wt::WLineEdit>()};
|
||||
password->setEchoMode(Wt::EchoMode::Password);
|
||||
password->setAttributeValue("autocomplete", "new-password");
|
||||
t->setFormWidget(SettingsModel::PasswordField, std::move(password));
|
||||
|
||||
// Password confirm
|
||||
auto passwordConfirm {std::make_unique<Wt::WLineEdit>()};
|
||||
passwordConfirm->setEchoMode(Wt::EchoMode::Password);
|
||||
passwordConfirm->setAttributeValue("autocomplete", "new-password");
|
||||
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
|
||||
}
|
||||
|
||||
// Password
|
||||
auto password {std::make_unique<Wt::WLineEdit>()};
|
||||
password->setEchoMode(Wt::EchoMode::Password);
|
||||
password->setAttributeValue("autocomplete", "new-password");
|
||||
t->setFormWidget(SettingsModel::PasswordField, std::move(password));
|
||||
|
||||
// Password confirm
|
||||
auto passwordConfirm {std::make_unique<Wt::WLineEdit>()};
|
||||
passwordConfirm->setEchoMode(Wt::EchoMode::Password);
|
||||
passwordConfirm->setAttributeValue("autocomplete", "new-password");
|
||||
t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm));
|
||||
|
||||
// Audio
|
||||
{
|
||||
// Transcode
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "common/DirectoryValidator.hpp"
|
||||
#include "common/MandatoryValidator.hpp"
|
||||
#include "common/ValueStringModel.hpp"
|
||||
#include "ScannerController.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
@@ -66,7 +67,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
addField(RecommendationEngineTypeField);
|
||||
addField(TagsField);
|
||||
|
||||
auto dirValidator {std::make_shared<DirectoryValidator>()};
|
||||
auto dirValidator {createDirectoryValidator()};
|
||||
dirValidator->setMandatory(true);
|
||||
setValidator(MediaDirectoryField, dirValidator);
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "common/Validators.hpp"
|
||||
#include "common/AuthModeModel.hpp"
|
||||
#include "common/LoginNameValidator.hpp"
|
||||
#include "common/MandatoryValidator.hpp"
|
||||
#include "common/PasswordValidator.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
@@ -42,118 +43,63 @@ class InitWizardModel : public Wt::WFormModel
|
||||
public:
|
||||
|
||||
// Associate each field with a unique string literal.
|
||||
static const Field AdminLoginField;
|
||||
static const Field PasswordField;
|
||||
static const Field PasswordConfirmField;
|
||||
static inline const Field AuthModeField{"auth-mode"};
|
||||
static inline const Field AdminLoginField {"admin-login"};
|
||||
static inline const Field PasswordField {"password"};
|
||||
static inline const Field PasswordConfirmField {"password-confirm"};
|
||||
|
||||
InitWizardModel() : Wt::WFormModel()
|
||||
{
|
||||
addField(AdminLoginField);
|
||||
addField(AuthModeField);
|
||||
addField(PasswordField);
|
||||
addField(PasswordConfirmField);
|
||||
|
||||
setValidator(AuthModeField, createMandatoryValidator());
|
||||
setValidator(AdminLoginField, createNameValidator());
|
||||
setValidator(PasswordField, createMandatoryValidator());
|
||||
setValidator(AdminLoginField, createLoginNameValidator());
|
||||
setValidator(PasswordField, createPasswordStrengthValidator([this] { return valueText(AdminLoginField).toUTF8(); }));
|
||||
validator(PasswordField)->setMandatory(true);
|
||||
setValidator(PasswordConfirmField, createMandatoryValidator());
|
||||
}
|
||||
|
||||
std::shared_ptr<AuthModeModel> getAuthModeModel() const { return _authModeModel; }
|
||||
|
||||
void saveData()
|
||||
{
|
||||
const Database::User::PasswordHash passwordHash {Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8())};
|
||||
|
||||
auto transaction(LmsApp->getDbSession().createUniqueTransaction());
|
||||
|
||||
// Check if a user already exist
|
||||
// If it's the case, just do nothing
|
||||
if (!Database::User::getAll(LmsApp->getDbSession()).empty())
|
||||
throw LmsException("Admin user already created");
|
||||
|
||||
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))};
|
||||
if (!authModeRow)
|
||||
throw LmsException {"Bad authentication mode"};
|
||||
|
||||
const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)};
|
||||
throw LmsException {"Admin user already created"};
|
||||
|
||||
Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(AdminLoginField).toUTF8())};
|
||||
user.modify()->setType(Database::User::Type::ADMIN);
|
||||
user.modify()->setAuthMode(authMode);
|
||||
if (authMode == Database::User::AuthMode::Internal)
|
||||
user.modify()->setPasswordHash(passwordHash);
|
||||
}
|
||||
|
||||
void validatePassword(Wt::WString& error) const
|
||||
{
|
||||
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))};
|
||||
if (!authModeRow)
|
||||
throw LmsException {"Bad authentication mode"};
|
||||
|
||||
const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)};
|
||||
if (authMode != Database::User::AuthMode::Internal)
|
||||
return;
|
||||
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
// Evaluate the strength of the password
|
||||
if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
else
|
||||
error = Wt::WString::tr("Lms.password-must-not-be-empty");
|
||||
}
|
||||
|
||||
void validatePasswordConfirm(Wt::WString& error) const
|
||||
{
|
||||
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))};
|
||||
if (!authModeRow)
|
||||
throw LmsException {"Bad authentication mode"};
|
||||
|
||||
const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)};
|
||||
if (authMode != Database::User::AuthMode::Internal)
|
||||
return;
|
||||
|
||||
if (validation(PasswordField).state() == Wt::ValidationState::Valid)
|
||||
{
|
||||
if (valueText(PasswordField) != valueText(PasswordConfirmField))
|
||||
error = Wt::WString::tr("Lms.passwords-dont-match");
|
||||
}
|
||||
Service<::Auth::IPasswordService>::get()->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
{
|
||||
Wt::WString error;
|
||||
|
||||
if (field == PasswordField)
|
||||
if (field == PasswordConfirmField)
|
||||
{
|
||||
validatePassword(error);
|
||||
}
|
||||
else if (field == PasswordConfirmField)
|
||||
{
|
||||
validatePasswordConfirm(error);
|
||||
if (validation(PasswordField).state() == Wt::ValidationState::Valid
|
||||
&& valueText(PasswordField) != valueText(PasswordConfirmField))
|
||||
{
|
||||
error = Wt::WString::tr("Lms.passwords-dont-match");
|
||||
}
|
||||
else
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Wt::WFormModel::validateField(field);
|
||||
}
|
||||
|
||||
setValidation(field, Wt::WValidator::Result( error.empty() ? Wt::ValidationState::Valid : Wt::ValidationState::Invalid, error));
|
||||
setValidation(field, Wt::WValidator::Result {Wt::ValidationState::Invalid, error});
|
||||
|
||||
return (validation(field).state() == Wt::ValidationState::Valid);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::shared_ptr<AuthModeModel> _authModeModel {createAuthModeModel()};
|
||||
};
|
||||
|
||||
const Wt::WFormModel::Field InitWizardModel::AdminLoginField = "admin-login";
|
||||
const Wt::WFormModel::Field InitWizardModel::PasswordField = "password";
|
||||
const Wt::WFormModel::Field InitWizardModel::PasswordConfirmField = "password-confirm";
|
||||
|
||||
InitWizardView::InitWizardView()
|
||||
: Wt::WTemplateFormView(Wt::WString::tr("Lms.Admin.InitWizard.template"))
|
||||
: Wt::WTemplateFormView {Wt::WString::tr("Lms.Admin.InitWizard.template")}
|
||||
{
|
||||
auto model = std::make_shared<InitWizardModel>();
|
||||
|
||||
@@ -164,20 +110,6 @@ InitWizardView::InitWizardView()
|
||||
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
|
||||
{
|
||||
auto passwordEdit = std::make_unique<Wt::WLineEdit>();
|
||||
|
||||
+49
-112
@@ -36,9 +36,8 @@
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "common/AuthModeModel.hpp"
|
||||
#include "common/Validators.hpp"
|
||||
#include "common/ValueStringModel.hpp"
|
||||
#include "common/LoginNameValidator.hpp"
|
||||
#include "common/PasswordValidator.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
|
||||
@@ -53,122 +52,62 @@ class UserModel : public Wt::WFormModel
|
||||
static inline const Field LoginField {"login"};
|
||||
static inline const Field PasswordField {"password"};
|
||||
static inline const Field DemoField {"demo"};
|
||||
static inline const Field AuthModeField{"auth-mode"};
|
||||
|
||||
using AuthModeModel = ValueStringModel<User::AuthMode>;
|
||||
|
||||
UserModel(std::optional<Database::IdType> userId)
|
||||
UserModel(std::optional<Database::IdType> userId, ::Auth::IPasswordService* authPasswordService)
|
||||
: _userId {userId}
|
||||
, _authPasswordService {authPasswordService}
|
||||
{
|
||||
if (!_userId)
|
||||
{
|
||||
addField(LoginField);
|
||||
setValidator(LoginField, createNameValidator());
|
||||
setValidator(LoginField, createLoginNameValidator());
|
||||
}
|
||||
|
||||
addField(AuthModeField);
|
||||
addField(PasswordField);
|
||||
if (authPasswordService)
|
||||
{
|
||||
addField(PasswordField);
|
||||
setValidator(PasswordField, createPasswordStrengthValidator([this] { return getLoginName(); }));
|
||||
if (!userId)
|
||||
validator(PasswordField)->setMandatory(true);
|
||||
}
|
||||
addField(DemoField);
|
||||
|
||||
setValidator(AuthModeField, createMandatoryValidator());
|
||||
|
||||
loadData();
|
||||
}
|
||||
|
||||
std::shared_ptr<AuthModeModel> getAuthModeModel() const { return _authModeModel; }
|
||||
|
||||
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()};
|
||||
|
||||
if (_userId)
|
||||
{
|
||||
// Update user
|
||||
Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)};
|
||||
if (!user)
|
||||
throw UserNotFoundException {*_userId};
|
||||
|
||||
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))};
|
||||
if (!authModeRow)
|
||||
throw LmsException {"Bad authentication mode"};
|
||||
|
||||
const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)};
|
||||
user.modify()->setAuthMode(authMode);
|
||||
if (authMode == Database::User::AuthMode::Internal && passwordHash)
|
||||
{
|
||||
user.modify()->setPasswordHash(*passwordHash);
|
||||
user.modify()->clearAuthTokens();
|
||||
}
|
||||
if (_authPasswordService && !valueText(PasswordField).empty())
|
||||
_authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check races with other endpoints (subsonic API...)
|
||||
Database::User::pointer user {Database::User::getByLoginName(LmsApp->getDbSession(), valueText(LoginField).toUTF8())};
|
||||
if (user)
|
||||
throw UserNotAllowedException {};
|
||||
|
||||
// Create user
|
||||
Database::User::pointer user {Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8())};
|
||||
user = Database::User::create(LmsApp->getDbSession(), valueText(LoginField).toUTF8());
|
||||
|
||||
if (Wt::asNumber(value(DemoField)))
|
||||
user.modify()->setType(Database::User::Type::DEMO);
|
||||
|
||||
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))};
|
||||
if (!authModeRow)
|
||||
throw LmsException {"Bad authentication mode"};
|
||||
|
||||
const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)};
|
||||
user.modify()->setAuthMode(authMode);
|
||||
if (authMode == Database::User::AuthMode::Internal)
|
||||
user.modify()->setPasswordHash(*passwordHash);
|
||||
if (_authPasswordService)
|
||||
_authPasswordService->setPassword(LmsApp->getDbSession(), user.id(), valueText(PasswordField).toUTF8());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
void validatePassword(Wt::WString& error) const
|
||||
{
|
||||
auto authModeRow {_authModeModel->getRowFromString(valueText(AuthModeField))};
|
||||
if (!authModeRow)
|
||||
throw LmsException {"Bad authentication mode"};
|
||||
|
||||
const Database::User::AuthMode authMode {_authModeModel->getValue(*authModeRow)};
|
||||
if (authMode != Database::User::AuthMode::Internal)
|
||||
return;
|
||||
|
||||
if (!valueText(PasswordField).empty())
|
||||
{
|
||||
if (Wt::asNumber(value(DemoField)))
|
||||
{
|
||||
// Demo account: password must be the same as the login name
|
||||
if (valueText(PasswordField) != getLoginName())
|
||||
error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Evaluate the strength of the password for non demo accounts
|
||||
if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8()))
|
||||
error = Wt::WString::tr("Lms.password-too-weak");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
bool needPassword {true};
|
||||
|
||||
// Allow an empty password if and only if the user previously had one set
|
||||
if (_userId)
|
||||
{
|
||||
const Database::User::pointer user {Database::User::getById(LmsApp->getDbSession(), *_userId)};
|
||||
if (!user)
|
||||
throw UserNotFoundException {*_userId};
|
||||
|
||||
needPassword = user->getPasswordHash().hash.empty();
|
||||
}
|
||||
|
||||
if (needPassword)
|
||||
error = Wt::WString::tr("Lms.password-must-not-be-empty");
|
||||
}
|
||||
}
|
||||
|
||||
void loadData()
|
||||
{
|
||||
if (!_userId)
|
||||
@@ -181,14 +120,6 @@ class UserModel : public Wt::WFormModel
|
||||
throw UserNotFoundException {*_userId};
|
||||
else if (user == LmsApp->getUser())
|
||||
throw UserNotAllowedException {};
|
||||
|
||||
auto authModeRow {_authModeModel->getRowFromValue(user->getAuthMode())};
|
||||
if (authModeRow)
|
||||
{
|
||||
setValue(AuthModeField, _authModeModel->getString(*authModeRow));
|
||||
if (_authModeModel->getValue(*authModeRow) != User::AuthMode::Internal)
|
||||
setReadOnly(PasswordField, true);
|
||||
}
|
||||
}
|
||||
|
||||
std::string getLoginName() const
|
||||
@@ -204,6 +135,16 @@ class UserModel : public Wt::WFormModel
|
||||
return valueText(LoginField).toUTF8();
|
||||
}
|
||||
|
||||
void validatePassword(Wt::WString& error) const
|
||||
{
|
||||
if (!valueText(PasswordField).empty() && Wt::asNumber(value(DemoField)))
|
||||
{
|
||||
// Demo account: password must be the same as the login name
|
||||
if (valueText(PasswordField) != getLoginName())
|
||||
error = Wt::WString::tr("Lms.Admin.User.demo-password-invalid");
|
||||
}
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
{
|
||||
Wt::WString error;
|
||||
@@ -237,7 +178,7 @@ class UserModel : public Wt::WFormModel
|
||||
}
|
||||
|
||||
std::optional<Database::IdType> _userId;
|
||||
std::shared_ptr<AuthModeModel> _authModeModel {createAuthModeModel()};
|
||||
::Auth::IPasswordService* _authPasswordService {};
|
||||
};
|
||||
|
||||
UserView::UserView()
|
||||
@@ -262,7 +203,11 @@ UserView::refreshView()
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -284,31 +229,23 @@ UserView::refreshView()
|
||||
t->bindString("title", Wt::WString::tr("Lms.Admin.User.user-create"));
|
||||
}
|
||||
|
||||
// Auth mode
|
||||
auto authMode = std::make_unique<Wt::WComboBox>();
|
||||
authMode->setModel(model->getAuthModeModel());
|
||||
authMode->activated().connect([=](int row)
|
||||
if (authPasswordService)
|
||||
{
|
||||
const User::AuthMode authMode {model->getAuthModeModel()->getValue(row)};
|
||||
t->setCondition("if-has-password", true);
|
||||
|
||||
model->setReadOnly(UserModel::PasswordField, authMode != User::AuthMode::Internal);
|
||||
t->updateModel(model.get());
|
||||
t->updateView(model.get());
|
||||
});
|
||||
t->setFormWidget(UserModel::AuthModeField, std::move(authMode));
|
||||
|
||||
// Password
|
||||
auto passwordEdit = std::make_unique<Wt::WLineEdit>();
|
||||
passwordEdit->setEchoMode(Wt::EchoMode::Password);
|
||||
passwordEdit->setAttributeValue("autocomplete", "off");
|
||||
t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit));
|
||||
// Password
|
||||
auto passwordEdit = std::make_unique<Wt::WLineEdit>();
|
||||
passwordEdit->setEchoMode(Wt::EchoMode::Password);
|
||||
passwordEdit->setAttributeValue("autocomplete", "off");
|
||||
t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit));
|
||||
}
|
||||
|
||||
// Demo account
|
||||
t->setFormWidget(UserModel::DemoField, std::make_unique<Wt::WCheckBox>());
|
||||
if (!userId && Service<IConfig>::get()->getBool("demo", false))
|
||||
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([=]()
|
||||
{
|
||||
t->updateModel(model.get());
|
||||
|
||||
@@ -23,9 +23,11 @@
|
||||
#include <Wt/WMessageBox.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "auth/IPasswordService.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
@@ -38,11 +40,16 @@ UsersView::UsersView()
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("users");
|
||||
|
||||
Wt::WPushButton* addBtn = bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Admin.Users.add"));
|
||||
addBtn->clicked().connect([]()
|
||||
if (Service<::Auth::IPasswordService>::get() && Service<::Auth::IPasswordService>::get()->canSetPasswords())
|
||||
{
|
||||
LmsApp->setInternalPath("/admin/user", true);
|
||||
});
|
||||
setCondition("if-can-create-user", true);
|
||||
|
||||
Wt::WPushButton* addBtn = bindNew<Wt::WPushButton>("add-btn", Wt::WString::tr("Lms.Admin.Users.add"));
|
||||
addBtn->clicked().connect([]()
|
||||
{
|
||||
LmsApp->setInternalPath("/admin/user", true);
|
||||
});
|
||||
}
|
||||
|
||||
wApp->internalPathChanged().connect(this, [this]()
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -19,14 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef LMS_SUPPORT_PAM
|
||||
#include <Wt/WValidator.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace Auth::PAM
|
||||
namespace UserInterface
|
||||
{
|
||||
bool checkUserPassword(const std::string& loginName, const std::string& password);
|
||||
}
|
||||
|
||||
#endif // LMS_SUPPORT_PAM
|
||||
std::shared_ptr<Wt::WValidator> createDirectoryValidator();
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -17,25 +17,20 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WValidator.h>
|
||||
#include "LoginNameValidator.hpp"
|
||||
|
||||
#include <Wt/WLengthValidator.h>
|
||||
#include "database/User.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
std::shared_ptr<Wt::WValidator> createNameValidator();
|
||||
std::shared_ptr<Wt::WValidator> createMandatoryValidator();
|
||||
|
||||
class DirectoryValidator : public Wt::WValidator
|
||||
namespace UserInterface
|
||||
{
|
||||
public:
|
||||
DirectoryValidator();
|
||||
|
||||
Wt::WValidator::Result validate(const Wt::WString& input) const override;
|
||||
|
||||
};
|
||||
|
||||
std::shared_ptr<Wt::WValidator>
|
||||
createLoginNameValidator()
|
||||
{
|
||||
auto v = std::make_unique<Wt::WLengthValidator>();
|
||||
v->setMandatory(true);
|
||||
v->setMinimumLength(::Database::User::MinNameLength);
|
||||
v->setMaximumLength(::Database::User::MaxNameLength);
|
||||
return v;
|
||||
}
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
* Copyright (C) 2013 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
@@ -19,13 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/User.hpp"
|
||||
#include "common/ValueStringModel.hpp"
|
||||
#include <Wt/WValidator.h>
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
using AuthModeModel = ValueStringModel<Database::User::AuthMode>;
|
||||
|
||||
std::unique_ptr<AuthModeModel> createAuthModeModel();
|
||||
}
|
||||
std::shared_ptr<Wt::WValidator> createLoginNameValidator();
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -17,26 +17,17 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AuthModeModel.hpp"
|
||||
#pragma once
|
||||
|
||||
#include "auth/IPasswordService.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include <Wt/WValidator.h>
|
||||
|
||||
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>
|
||||
createAuthModeModel()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
// Check current user password
|
||||
std::shared_ptr<Wt::WValidator> createPasswordCheckValidator();
|
||||
} // namespace UserInterface
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
|
||||
add_subdirectory(database)
|
||||
add_subdirectory(som)
|
||||
add_subdirectory(utils)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user