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

This commit is contained in:
emeric
2021-03-04 19:32:09 +01:00
parent b94fe3e852
commit cc28d893f5
73 changed files with 2208 additions and 1254 deletions
+7 -2
View File
@@ -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)
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AuthServiceBase.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
Database::IdType
AuthServiceBase::getOrCreateUser(Database::Session& session, std::string_view loginName)
{
auto transaction {session.createUniqueTransaction()};
Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
if (!user)
{
const Database::User::Type type {Database::User::getCount(session) == 0 ? Database::User::Type::ADMIN : Database::User::Type::REGULAR};
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == Database::User::Type::ADMIN);
user = Database::User::create(session, loginName);
user.modify()->setType(type);
}
return user.id();
}
void
AuthServiceBase::onUserAuthenticated(Database::Session& session, Database::IdType userId)
{
auto transaction {session.createUniqueTransaction()};
Database::User::pointer user {Database::User::getById(session, userId)};
if (user)
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
}
}
+38
View File
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
#include "database/Types.hpp"
namespace Database
{
class Session;
}
namespace Auth
{
class AuthServiceBase
{
protected:
Database::IdType getOrCreateUser(Database::Session& session, std::string_view loginName);
void onUserAuthenticated(Database::Session& session, Database::IdType userId);
};
}
+89 -77
View File
@@ -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
+11 -13
View File
@@ -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;
};
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "auth/IEnvService.hpp"
#include "auth/Types.hpp"
#include "http-headers/HttpHeadersEnvService.hpp"
namespace Auth
{
std::unique_ptr<IEnvService>
createEnvService(std::string_view backendName)
{
if (backendName == "http-headers")
return std::make_unique<HttpHeadersEnvService>();
throw Exception {"Authentication backend '" + std::string {backendName} + "' is not supported!"};
}
}
-156
View File
@@ -1,156 +0,0 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PasswordService.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include <Wt/WRandom.h>
#include "database/Session.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#ifdef LMS_SUPPORT_PAM
#include "pam/PAM.hpp"
#endif
namespace Auth {
std::unique_ptr<IPasswordService> createPasswordService(std::size_t maxThrottlerEntries)
{
return std::make_unique<PasswordService>(maxThrottlerEntries);
}
PasswordService::PasswordService(std::size_t maxThrottlerEntries)
: _loginThrottler{maxThrottlerEntries}
{
}
bool
PasswordService::isAuthModeSupported(Database::User::AuthMode authMode) const
{
switch (authMode)
{
case Database::User::AuthMode::Internal:
return true;
case Database::User::AuthMode::PAM:
#ifdef LMS_SUPPORT_PAM
return true;
#else
return false;
#endif
}
return false;
}
static
bool
checkUserPassword(Database::Session& session, const std::string& loginName, const std::string& password)
{
Database::User::AuthMode authMode;
Database::User::PasswordHash passwordHash;
{
auto transaction {session.createSharedTransaction()};
const Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
if (!user)
return false;
authMode = user->getAuthMode();
passwordHash = user->getPasswordHash();
}
switch (authMode)
{
case Database::User::AuthMode::Internal:
{
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
const Wt::Auth::BCryptHashFunction hashFunc {7}; // TODO parametrize this
return hashFunc.verify(password, passwordHash.salt, passwordHash.hash);
}
case Database::User::AuthMode::PAM:
#ifdef LMS_SUPPORT_PAM
return PAM::checkUserPassword(loginName, password);
#else
return false;
#endif
}
return false;
}
PasswordService::PasswordCheckResult
PasswordService::checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password)
{
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock<std::shared_timed_mutex> lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return PasswordCheckResult::Throttled;
}
const bool match {Auth::checkUserPassword(session, loginName, password)};
{
std::unique_lock<std::shared_timed_mutex> lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return PasswordCheckResult::Throttled;
if (match)
{
_loginThrottler.onGoodClientAttempt(clientAddress);
return PasswordCheckResult::Match;
}
else
{
_loginThrottler.onBadClientAttempt(clientAddress);
return PasswordCheckResult::Mismatch;
}
}
}
Database::User::PasswordHash
PasswordService::hashPassword(const std::string& password) const
{
const std::string salt {Wt::WRandom::generateId(32)};
const Wt::Auth::BCryptHashFunction hashFunc {6};
return {salt, hashFunc.compute(password, salt)};
}
bool
PasswordService::evaluatePasswordStrength(const std::string& loginName, const std::string& password) const
{
Wt::Auth::PasswordStrengthValidator validator;
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4);
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::ThreeCharClass, 4);
validator.setMinimumLength(Wt::Auth::PasswordStrengthType::FourCharClass, 4);
validator.setMinimumPassPhraseWords(1);
validator.setMinimumMatchLength(3);
return validator.evaluateStrength(password, loginName, "").isValid();
}
} // namespace Auth
-57
View File
@@ -1,57 +0,0 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#include "auth/IPasswordService.hpp"
#include "LoginThrottler.hpp"
namespace Database
{
class Session;
}
namespace Auth {
class PasswordService : public IPasswordService
{
public:
PasswordService(std::size_t maxThrottlerEntries);
PasswordService(const PasswordService&) = delete;
PasswordService& operator=(const PasswordService&) = delete;
PasswordService(PasswordService&&) = delete;
PasswordService& operator=(PasswordService&&) = delete;
private:
bool isAuthModeSupported(Database::User::AuthMode authMode) const override;
PasswordCheckResult checkUserPassword(Database::Session& session, const boost::asio::ip::address& clientAddress, const std::string& loginName, const std::string& password) override;
Database::User::PasswordHash hashPassword(const std::string& password) const override;
bool evaluatePasswordStrength(const std::string& loginName, const std::string& password) const override;
std::shared_timed_mutex _mutex;
LoginThrottler _loginThrottler;
};
}
@@ -0,0 +1,99 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PasswordServiceBase.hpp"
#include <Wt/Auth/HashFunction.h>
#include <Wt/WRandom.h>
#include "internal/InternalPasswordService.hpp"
#ifdef LMS_SUPPORT_PAM
#include "pam/PAMPasswordService.hpp"
#endif // LMS_SUPPORT_PAM
#include "auth/Types.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
static const Wt::Auth::SHA1HashFunction sha1Function;
std::unique_ptr<IPasswordService>
createPasswordService(std::string_view passwordAuthenticationBackend, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
{
if (passwordAuthenticationBackend == "internal")
return std::make_unique<InternalPasswordService>(maxThrottlerEntries, authTokenService);
#ifdef LMS_SUPPORT_PAM
else if (passwordAuthenticationBackend == "pam")
return std::make_unique<PAMPasswordService>(maxThrottlerEntries, authTokenService);
#endif // LMS_SUPPORT_PAM
throw Exception {"Authentication backend '" + std::string {passwordAuthenticationBackend} + "' is not supported!"};
}
PasswordServiceBase::PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
: _loginThrottler {maxThrottlerEntries}
, _authTokenService {authTokenService}
{
}
PasswordServiceBase::CheckResult
PasswordServiceBase::checkUserPassword(Database::Session& session,
const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password)
{
LMS_LOG(AUTH, DEBUG) << "Checking password for user '" << loginName << "'";
// Do not waste too much resource on brute force attacks (optim)
{
std::shared_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return {CheckResult::State::Throttled};
}
const bool match {checkUserPassword(session, loginName, password)};
{
std::unique_lock lock {_mutex};
if (_loginThrottler.isClientThrottled(clientAddress))
return {CheckResult::State::Throttled};
if (match)
{
_loginThrottler.onGoodClientAttempt(clientAddress);
const Database::IdType userId {getOrCreateUser(session, loginName)};
onUserAuthenticated(session, userId);
return {CheckResult::State::Granted, userId};
}
else
{
_loginThrottler.onBadClientAttempt(clientAddress);
return {CheckResult::State::Denied};
}
}
}
} // namespace Auth
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#include "auth/IPasswordService.hpp"
#include "AuthServiceBase.hpp"
#include "LoginThrottler.hpp"
namespace Database
{
class Session;
}
namespace Auth
{
class PasswordServiceBase : public IPasswordService, public AuthServiceBase
{
public:
PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
PasswordServiceBase(const PasswordServiceBase&) = delete;
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
PasswordServiceBase(PasswordServiceBase&&) = delete;
PasswordServiceBase& operator=(PasswordServiceBase&&) = delete;
protected:
IAuthTokenService& getAuthTokenService() { return _authTokenService; }
private:
virtual bool checkUserPassword(Database::Session& session,
std::string_view loginName,
std::string_view password) = 0;
CheckResult checkUserPassword(Database::Session& session,
const boost::asio::ip::address& clientAddress,
std::string_view loginName,
std::string_view password) override;
std::shared_mutex _mutex;
LoginThrottler _loginThrottler;
IAuthTokenService& _authTokenService;
};
} // namespace Auth
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "HttpHeadersEnvService.hpp"
#include <Wt/WEnvironment.h>
#include "utils/IConfig.hpp"
#include "utils/Logger.hpp"
#include "utils/Service.hpp"
namespace Auth
{
HttpHeadersEnvService::HttpHeadersEnvService()
: _fieldName {Service<IConfig>::get()->getString("http-headers-field-name", "X-Forwarded-User")}
{
LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'";
}
HttpHeadersEnvService::CheckResult
HttpHeadersEnvService::processEnv(Database::Session& session, const Wt::WEnvironment& env)
{
const std::string loginName { env.headerValue(_fieldName)};
if (loginName.empty())
return {CheckResult::State::Denied};
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
const Database::IdType userId {getOrCreateUser(session, loginName)};
onUserAuthenticated(session, userId);
return {CheckResult::State::Granted, userId};
}
} // namespace Auth
@@ -0,0 +1,39 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "auth/IEnvService.hpp"
#include "AuthServiceBase.hpp"
namespace Auth
{
class HttpHeadersEnvService : public IEnvService, public AuthServiceBase
{
public:
HttpHeadersEnvService();
private:
CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) override;
std::string _fieldName;
};
} // namespace Auth
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "InternalPasswordService.hpp"
#include <Wt/WRandom.h>
#include "auth/IAuthTokenService.hpp"
#include "auth/Types.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
InternalPasswordService::InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
: PasswordServiceBase {maxThrottlerEntries, authTokenService}
{
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::PassPhrase, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::ThreeCharClass, 4);
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::FourCharClass, 4);
_validator.setMinimumPassPhraseWords(1);
_validator.setMinimumMatchLength(3);
}
bool
InternalPasswordService::checkUserPassword(Database::Session& session,
std::string_view loginName,
std::string_view password)
{
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
Database::User::PasswordHash passwordHash;
{
auto transaction {session.createSharedTransaction()};
const Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
if (!user)
{
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
// hash random stuff here to waste some time
hashRandomPassword();
return false;
}
// Don't allow users being created or coming from other backends
passwordHash = user->getPasswordHash();
if (passwordHash.salt.empty() || passwordHash.hash.empty())
{
// hash random stuff here to waste some time
hashRandomPassword();
return false;
}
}
return _hashFunc.verify(std::string {password}, std::string {passwordHash.salt}, std::string {passwordHash.hash});
}
bool
InternalPasswordService::canSetPasswords() const
{
return true;
}
bool
InternalPasswordService::isPasswordSecureEnough(std::string_view loginName, std::string_view password) const
{
return _validator.evaluateStrength(std::string {password}, std::string {loginName}, "").isValid();
}
void
InternalPasswordService::setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword)
{
const Database::User::PasswordHash passwordHash {hashPassword(newPassword)};
auto transaction {session.createUniqueTransaction()};
const Database::User::pointer user {Database::User::getById(session, userId)};
if (!user)
throw Exception {"User not found!"};
if (!isPasswordSecureEnough(user->getLoginName(), newPassword))
throw PasswordTooWeakException {};
user.modify()->setPasswordHash(passwordHash);
getAuthTokenService().clearAuthTokens(session, userId);
}
Database::User::PasswordHash
InternalPasswordService::hashPassword(std::string_view password) const
{
const std::string salt {Wt::WRandom::generateId(32)};
return {salt, _hashFunc.compute(std::string {password}, salt)};
}
void
InternalPasswordService::hashRandomPassword() const
{
hashPassword(Wt::WRandom::generateId(32));
}
} // namespace Auth
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Auth/HashFunction.h>
#include <Wt/Auth/PasswordStrengthValidator.h>
#include "database/User.hpp"
#include "PasswordServiceBase.hpp"
#include "LoginThrottler.hpp"
namespace Auth
{
class IAuthTokenService;
class InternalPasswordService : public PasswordServiceBase
{
public:
InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
private:
bool checkUserPassword(Database::Session& session,
std::string_view loginName,
std::string_view password) override;
bool canSetPasswords() const override;
bool isPasswordSecureEnough(std::string_view loginName, std::string_view password) const override;
void setPassword(Database::Session& session, Database::IdType userId, std::string_view newPassword) override;
Database::User::PasswordHash hashPassword(std::string_view password) const;
void hashRandomPassword() const;
const Wt::Auth::BCryptHashFunction _hashFunc {7}; // TODO parametrize this
Wt::Auth::PasswordStrengthValidator _validator;
};
}
-183
View File
@@ -1,183 +0,0 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PAM.hpp"
#include <cstring>
#include "utils/Exception.hpp"
#include "utils/Logger.hpp"
#include <security/pam_appl.h>
namespace Auth::PAM
{
class PAMError
{
public:
PAMError(std::string_view msg, pam_handle_t *pamh, int err)
{
_errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err);
}
std::string_view message() const { return _errorMsg; }
private:
std::string _errorMsg;
};
class PAMContext
{
public:
PAMContext(std::string_view loginName)
{
int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)};
if (err != PAM_SUCCESS)
throw PAMError {"start failed", _pamh, err};
}
~PAMContext()
{
int err {pam_end(_pamh, 0)};
if (err != PAM_SUCCESS)
LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err);
}
void authenticate(std::string_view password)
{
AuthenticateConvContext authContext {password};
ScopedConvContextSetter scopedContext {*this, authContext};
int err {pam_authenticate(_pamh, 0)};
if (err != PAM_SUCCESS)
throw PAMError {"authenticate failed", _pamh, err};
}
void validateAccount()
{
int err {pam_acct_mgmt(_pamh, PAM_SILENT)};
if (err != PAM_SUCCESS)
throw PAMError {"acct_mgmt failed", _pamh, err};
}
private:
class ConvContext
{
public:
virtual ~ConvContext() = default;
};
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password {password} {}
std::string_view getPassword() const { return _password; }
private:
std::string_view _password;
};
class ScopedConvContextSetter
{
public:
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
: _pamContext {pamContext}
{
_pamContext._convContext = &convContext;
}
~ScopedConvContextSetter()
{
_pamContext._convContext = nullptr;
}
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
private:
PAMContext& _pamContext;
};
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
{
if (msgCount < 1)
return PAM_CONV_ERR;
if (!resps || !msgs || !userData)
return PAM_CONV_ERR;
PAMContext& context {*static_cast<PAMContext*>(userData)};
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
if (!authenticateContext)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv!";
return PAM_CONV_ERR;
}
// Only expect a PAM_PROMPT_ECHO_OFF msg
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount;
return PAM_CONV_ERR;
}
pam_response* response {static_cast<pam_response*>(malloc(sizeof(pam_response)))};
if (!response)
return PAM_CONV_ERR;
response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str());
*resps = response;
return PAM_SUCCESS;
}
ConvContext* _convContext {};
pam_conv _conv {&PAMContext::conv, this};
pam_handle_t *_pamh {};
};
bool
checkUserPassword(const std::string& loginName, const std::string& password)
{
try
{
LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'";
PAMContext pamContext {loginName};
pamContext.authenticate(password);
pamContext.validateAccount();
return true;
}
catch (const PAMError& error)
{
LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message();
return false;
}
}
} // namespace Auth::PAM
@@ -0,0 +1,204 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PAMPasswordService.hpp"
#ifndef LMS_SUPPORT_PAM
#error "Should not compile this"
#endif
#include <cstring>
#include <security/pam_appl.h>
#include "auth/Types.hpp"
#include "database/Session.hpp"
#include "utils/Logger.hpp"
namespace Auth
{
class PAMError
{
public:
PAMError(std::string_view msg, pam_handle_t *pamh, int err)
{
_errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err);
}
std::string_view message() const { return _errorMsg; }
private:
std::string _errorMsg;
};
class PAMContext
{
public:
PAMContext(std::string_view loginName)
{
int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)};
if (err != PAM_SUCCESS)
throw PAMError {"start failed", _pamh, err};
}
~PAMContext()
{
int err {pam_end(_pamh, 0)};
if (err != PAM_SUCCESS)
LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err);
}
void authenticate(std::string_view password)
{
AuthenticateConvContext authContext {password};
ScopedConvContextSetter scopedContext {*this, authContext};
int err {pam_authenticate(_pamh, 0)};
if (err != PAM_SUCCESS)
throw PAMError {"authenticate failed", _pamh, err};
}
void validateAccount()
{
int err {pam_acct_mgmt(_pamh, PAM_SILENT)};
if (err != PAM_SUCCESS)
throw PAMError {"acct_mgmt failed", _pamh, err};
}
private:
class ConvContext
{
public:
virtual ~ConvContext() = default;
};
class AuthenticateConvContext final : public ConvContext
{
public:
AuthenticateConvContext(std::string_view password) : _password {password} {}
std::string_view getPassword() const { return _password; }
private:
std::string_view _password;
};
class ScopedConvContextSetter
{
public:
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
: _pamContext {pamContext}
{
_pamContext._convContext = &convContext;
}
~ScopedConvContextSetter()
{
_pamContext._convContext = nullptr;
}
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
private:
PAMContext& _pamContext;
};
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
{
if (msgCount < 1)
return PAM_CONV_ERR;
if (!resps || !msgs || !userData)
return PAM_CONV_ERR;
PAMContext& context {*static_cast<PAMContext*>(userData)};
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
if (!authenticateContext)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv!";
return PAM_CONV_ERR;
}
// Only expect a PAM_PROMPT_ECHO_OFF msg
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
{
LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount;
return PAM_CONV_ERR;
}
pam_response* response {static_cast<pam_response*>(malloc(sizeof(pam_response)))};
if (!response)
return PAM_CONV_ERR;
response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str());
*resps = response;
return PAM_SUCCESS;
}
ConvContext* _convContext {};
pam_conv _conv {&PAMContext::conv, this};
pam_handle_t *_pamh {};
};
bool
PAMPasswordService::checkUserPassword(Database::Session& /*session*/, std::string_view loginName, std::string_view password)
{
try
{
LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'";
PAMContext pamContext {loginName};
pamContext.authenticate(password);
pamContext.validateAccount();
return true;
}
catch (const PAMError& error)
{
LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message();
return false;
}
}
bool
PAMPasswordService::canSetPasswords() const
{
return false;
}
bool
PAMPasswordService::isPasswordSecureEnough(std::string_view, std::string_view) const
{
throw NotImplementedException {};
}
void
PAMPasswordService::setPassword(Database::Session&, Database::IdType, std::string_view)
{
throw NotImplementedException {};
}
} // namespace Auth
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2019 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <shared_mutex>
#include "PasswordServiceBase.hpp"
namespace Auth
{
class PAMPasswordService: public PasswordServiceBase
{
public:
using PasswordServiceBase::PasswordServiceBase;
private:
bool checkUserPassword(Database::Session& session,
std::string_view loginName,
std::string_view password) override;
bool canSetPasswords() const override;
bool isPasswordSecureEnough(std::string_view loginName,
std::string_view password) const override;
void setPassword(Database::Session& session,
Database::IdType userId,
std::string_view newPassword) override;
};
}
@@ -23,6 +23,7 @@
#include <optional>
#include <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
+33 -18
View File
@@ -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);
}
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "utils/Exception.hpp"
namespace Auth
{
class Exception : public ::LmsException
{
using LmsException::LmsException;
};
class NotImplementedException : public Exception
{
public:
NotImplementedException() : Auth::Exception {"Not implemented"} {}
};
}
+1 -2
View File
@@ -9,6 +9,7 @@ target_include_directories(lmscover INTERFACE
target_include_directories(lmscover PRIVATE
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)
+39 -32
View File
@@ -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
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Emeric Poupon
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
@@ -19,14 +19,15 @@
#pragma once
#ifdef LMS_SUPPORT_PAM
#include <string>
namespace Auth::PAM
namespace Wt::Dbo
{
bool checkUserPassword(const std::string& loginName, const std::string& password);
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});
}
};
}
#endif // LMS_SUPPORT_PAM
+12 -3
View File
@@ -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);
+4 -3
View File
@@ -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;
+41 -47
View File
@@ -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
+6 -15
View File
@@ -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;
+104 -50
View File
@@ -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()};
+1
View File
@@ -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
+16 -24
View File
@@ -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;
}
+5 -5
View File
@@ -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();
}
+6 -7
View File
@@ -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;
};
+6 -2
View File
@@ -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;