OpenSubsonic API: added apiKey support, ref #544
This commit is contained in:
@@ -24,7 +24,8 @@
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
template<typename Class>
|
||||
// Tag can be used if you have multiple services sharing the same interface
|
||||
template<typename Class, typename Tag = Class>
|
||||
class Service
|
||||
{
|
||||
public:
|
||||
@@ -46,12 +47,12 @@ namespace lms::core
|
||||
|
||||
Class* operator->() const
|
||||
{
|
||||
return Service<Class>::get();
|
||||
return Service<Class, Tag>::get();
|
||||
}
|
||||
|
||||
Class& operator*() const
|
||||
{
|
||||
return *Service<Class>::get();
|
||||
return *Service<Class, Tag>::get();
|
||||
}
|
||||
|
||||
static Class* get() { return _service.get(); }
|
||||
|
||||
@@ -5,6 +5,7 @@ add_executable(test-core
|
||||
LiteralString.cpp
|
||||
Path.cpp
|
||||
RecursiveSharedMutex.cpp
|
||||
Service.cpp
|
||||
String.cpp
|
||||
TraceLogger.cpp
|
||||
Utils.cpp
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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 <gtest/gtest.h>
|
||||
|
||||
#include "core/Service.hpp"
|
||||
|
||||
namespace lms::core::tests
|
||||
{
|
||||
class IMyService
|
||||
{
|
||||
};
|
||||
|
||||
class MyService : public IMyService
|
||||
{
|
||||
};
|
||||
|
||||
class MyOtherService : public IMyService
|
||||
{
|
||||
};
|
||||
|
||||
class MyServiceTag
|
||||
{
|
||||
};
|
||||
class MyOtherServiceTag
|
||||
{
|
||||
};
|
||||
|
||||
TEST(Service, ctr)
|
||||
{
|
||||
EXPECT_FALSE(Service<IMyService>().exists());
|
||||
EXPECT_EQ(Service<IMyService>().get(), nullptr);
|
||||
|
||||
Service<IMyService> myService{ std::make_unique<MyService>() };
|
||||
|
||||
EXPECT_TRUE(Service<IMyService>().exists());
|
||||
EXPECT_EQ(Service<IMyService>().get(), myService.get());
|
||||
}
|
||||
|
||||
TEST(Service, tags)
|
||||
{
|
||||
Service<IMyService, MyServiceTag> myService{ std::make_unique<MyService>() };
|
||||
Service<IMyService, MyOtherServiceTag> myOtherService{ std::make_unique<MyOtherService>() };
|
||||
|
||||
EXPECT_FALSE(Service<IMyService>().exists());
|
||||
EXPECT_EQ(Service<IMyService>().get(), nullptr);
|
||||
|
||||
EXPECT_TRUE((Service<IMyService, MyServiceTag>().exists()));
|
||||
EXPECT_TRUE((Service<IMyService, MyOtherServiceTag>().exists()));
|
||||
EXPECT_EQ((Service<IMyService, MyServiceTag>().get()), myService.get());
|
||||
EXPECT_EQ((Service<IMyService, MyOtherServiceTag>().get()), myOtherService.get());
|
||||
}
|
||||
} // namespace lms::core::tests
|
||||
@@ -30,29 +30,65 @@
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
: _value{ value }
|
||||
AuthToken::AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user)
|
||||
: _domain{ domain }
|
||||
, _value{ value }
|
||||
, _expiry{ expiry }
|
||||
, _maxUseCount{ maxUseCount }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
AuthToken::pointer AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
AuthToken::pointer AuthToken::create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<AuthToken>{ new AuthToken{ value, expiry, user } });
|
||||
return session.getDboSession()->add(std::unique_ptr<AuthToken>{ new AuthToken{ domain, value, expiry, maxUseCount, user } });
|
||||
}
|
||||
|
||||
void AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ?", now);
|
||||
}
|
||||
|
||||
AuthToken::pointer AuthToken::find(Session& session, std::string_view value)
|
||||
std::size_t AuthToken::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<AuthToken>().where("value = ?").bind(value));
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM auth_token"));
|
||||
}
|
||||
|
||||
AuthToken::pointer AuthToken::find(Session& session, AuthTokenId id)
|
||||
{
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<AuthToken>>("SELECT a_t from auth_token a_t").where("a_t.id = ?").bind(id));
|
||||
}
|
||||
|
||||
AuthToken::pointer AuthToken::find(Session& session, std::string_view domain, std::string_view value)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->find<AuthToken>() };
|
||||
query.where("domain = ?").bind(domain);
|
||||
query.where("value = ?").bind(value);
|
||||
|
||||
return utils::fetchQuerySingleResult(query);
|
||||
}
|
||||
|
||||
void AuthToken::find(Session& session, std::string_view domain, UserId userId, std::function<void(const AuthToken::pointer&)> visitor)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->find<AuthToken>() };
|
||||
query.where("domain = ?").bind(domain);
|
||||
query.where("user_id = ?").bind(userId);
|
||||
|
||||
utils::forEachQueryResult(query, visitor);
|
||||
}
|
||||
|
||||
void AuthToken::removeExpiredTokens(Session& session, std::string_view domain, const Wt::WDateTime& now)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ? AND domain = ?", now, domain);
|
||||
}
|
||||
|
||||
void AuthToken::clearUserTokens(Session& session, std::string_view domain, UserId user)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE user_id = ? AND domain = ?", user, domain);
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 74 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 75 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -947,6 +947,21 @@ SELECT
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE media_library SET path = rtrim(path, '/') WHERE path LIKE '%/'");
|
||||
}
|
||||
|
||||
void migrateFromV74(Session& session)
|
||||
{
|
||||
// New auth token authentication for Subsonic API
|
||||
// Previous tokens are not usable any more, no problem since they are just used for the ui's "remember me" feature
|
||||
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD domain TEXT NOT NULL");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD use_count INTEGER NOT NULL");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD last_used TEXT");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD max_use_count INTEGER");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_user_idx");
|
||||
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_expiry_idx");
|
||||
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_value_idx");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -997,6 +1012,7 @@ SELECT
|
||||
{ 71, migrateFromV71 },
|
||||
{ 72, migrateFromV72 },
|
||||
{ 73, migrateFromV73 },
|
||||
{ 74, migrateFromV74 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -188,9 +188,9 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||
|
||||
@@ -97,10 +97,4 @@ namespace lms::db
|
||||
assert(isAudioBitrateAllowed(bitrate));
|
||||
_subsonicDefaultTranscodingOutputBitrate = bitrate;
|
||||
}
|
||||
|
||||
void User::clearAuthTokens()
|
||||
{
|
||||
_authTokens.clear();
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
@@ -26,41 +27,62 @@
|
||||
|
||||
#include "database/AuthTokenId.hpp"
|
||||
#include "database/Object.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Session;
|
||||
|
||||
class User;
|
||||
|
||||
class AuthToken final : public Object<AuthToken, AuthTokenId>
|
||||
{
|
||||
public:
|
||||
AuthToken() = default;
|
||||
|
||||
// Utility
|
||||
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
|
||||
static pointer find(Session& session, std::string_view value);
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, AuthTokenId tokenId);
|
||||
static pointer find(Session& session, std::string_view domain, std::string_view value);
|
||||
static void find(Session& session, std::string_view domain, UserId userId, std::function<void(const AuthToken::pointer&)> visitor);
|
||||
static void removeExpiredTokens(Session& session, std::string_view domain, const Wt::WDateTime& now);
|
||||
static void clearUserTokens(Session& session, std::string_view domain, UserId user);
|
||||
|
||||
// Accessors
|
||||
const Wt::WDateTime& getExpiry() const { return _expiry; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
const std::string& getValue() const { return _value; }
|
||||
std::size_t getUseCount() const { return _useCount; }
|
||||
Wt::WDateTime getLastUsed() const { return _lastUsed; }
|
||||
std::optional<std::size_t> getMaxUseCount() const { return _maxUseCount; }
|
||||
|
||||
// Setters
|
||||
std::size_t incUseCount() { return ++_useCount; }
|
||||
void setLastUsed(const Wt::WDateTime& lastUsed) { _lastUsed = lastUsed; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _domain, "domain");
|
||||
Wt::Dbo::field(a, _value, "value");
|
||||
Wt::Dbo::field(a, _expiry, "expiry");
|
||||
Wt::Dbo::field(a, _useCount, "use_count");
|
||||
Wt::Dbo::field(a, _lastUsed, "last_used");
|
||||
Wt::Dbo::field(a, _maxUseCount, "max_use_count");
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||
AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user);
|
||||
static pointer create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user);
|
||||
|
||||
std::string _domain;
|
||||
std::string _value;
|
||||
Wt::WDateTime _expiry;
|
||||
long _useCount{};
|
||||
Wt::WDateTime _lastUsed;
|
||||
std::optional<long> _maxUseCount;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace lms::db
|
||||
} // namespace lms::db
|
||||
@@ -108,7 +108,6 @@ namespace lms::db
|
||||
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
|
||||
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
|
||||
void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; }
|
||||
void clearAuthTokens();
|
||||
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
|
||||
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
|
||||
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2024 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 "Common.hpp"
|
||||
|
||||
#include "database/AuthToken.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedAuthToken = ScopedEntity<db::AuthToken>;
|
||||
|
||||
TEST_F(DatabaseFixture, AuthTokens)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(AuthToken::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedAuthToken token{ session, "myDomain", "foo", Wt::WDateTime{}, std::nullopt, user.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(AuthToken::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
AuthToken::clearUserTokens(session, "nonExistingDomain", user.getId());
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
add_executable(test-database
|
||||
AuthToken.cpp
|
||||
Artist.cpp
|
||||
Cluster.cpp
|
||||
Common.cpp
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "Common.hpp"
|
||||
|
||||
#include "core/String.hpp"
|
||||
#include "database/AuthToken.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/Image.hpp"
|
||||
@@ -337,6 +338,7 @@ VALUES
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_FALSE(Artist::find(session, ArtistId{}));
|
||||
EXPECT_FALSE(AuthToken::find(session, AuthTokenId{}));
|
||||
EXPECT_FALSE(Cluster::find(session, ClusterId{}));
|
||||
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
|
||||
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "AuthTokenService.hpp"
|
||||
|
||||
#include <Wt/Auth/HashFunction.h>
|
||||
#include <Wt/Auth/PasswordStrengthValidator.h>
|
||||
#include <Wt/WRandom.h>
|
||||
|
||||
#include "core/Exception.hpp"
|
||||
@@ -32,72 +31,95 @@
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
|
||||
namespace
|
||||
{
|
||||
return std::make_unique<AuthTokenService>(db, maxThrottlerEntries);
|
||||
AuthTokenService::AuthTokenInfo createAuthTokenInfo(const db::AuthToken::pointer& authToken)
|
||||
{
|
||||
return AuthTokenService::AuthTokenInfo{
|
||||
.userId = authToken->getUser()->getId(),
|
||||
.expiry = authToken->getExpiry(),
|
||||
.lastUsed = authToken->getLastUsed(),
|
||||
.useCount = authToken->getUseCount(),
|
||||
.maxUseCount = authToken->getMaxUseCount(),
|
||||
};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount)
|
||||
{
|
||||
return std::make_unique<AuthTokenService>(db, maxThrottlerEntryCount);
|
||||
}
|
||||
|
||||
static const Wt::Auth::SHA1HashFunction sha1Function;
|
||||
|
||||
AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries)
|
||||
AuthTokenService::AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount)
|
||||
: AuthServiceBase{ db }
|
||||
, _loginThrottler{ maxThrottlerEntries }
|
||||
, _loginThrottler{ maxThrottlerEntryCount }
|
||||
{
|
||||
}
|
||||
|
||||
std::string
|
||||
AuthTokenService::createAuthToken(db::UserId userId, const Wt::WDateTime& expiry)
|
||||
void AuthTokenService::registerDomain(core::LiteralString domain, const DomainParameters& params)
|
||||
{
|
||||
const std::string secret{ Wt::WRandom::generateId(32) };
|
||||
const std::string secretHash{ sha1Function.compute(secret, {}) };
|
||||
|
||||
db::Session& session{ getDbSession() };
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
db::User::pointer user{ db::User::find(session, userId) };
|
||||
if (!user)
|
||||
throw Exception{ "User deleted" };
|
||||
|
||||
db::AuthToken::pointer authToken{ session.create<db::AuthToken>(secretHash, expiry, user) };
|
||||
|
||||
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString());
|
||||
|
||||
if (user->getAuthTokensCount() >= 50)
|
||||
db::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
|
||||
|
||||
return secret;
|
||||
auto [it, inserted]{ _domainParameters.emplace(domain, params) };
|
||||
if (!inserted)
|
||||
throw Exception{ "Auth token domain already registered!" };
|
||||
}
|
||||
|
||||
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
|
||||
AuthTokenService::processAuthToken(std::string_view secret)
|
||||
void AuthTokenService::createAuthToken(core::LiteralString domain, db::UserId userId, std::string_view token)
|
||||
{
|
||||
const std::string secretHash{ sha1Function.compute(std::string{ secret }, {}) };
|
||||
const DomainParameters& params{ getDomainParameters(domain) };
|
||||
|
||||
db::Session& session{ getDbSession() };
|
||||
const auto now{ Wt::WDateTime::currentDateTime() };
|
||||
const auto expiry{ params.tokenDuration ? now.addSecs(std::chrono::duration_cast<std::chrono::seconds>(params.tokenDuration.value()).count()) : Wt::WDateTime{} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
const db::User::pointer user{ db::User::find(session, userId) };
|
||||
if (!user)
|
||||
throw Exception{ "User deleted" };
|
||||
|
||||
const db::AuthToken::pointer authToken{ session.create<db::AuthToken>(domain.str(), token, expiry, params.tokenMaxUseCount, user) };
|
||||
|
||||
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << authToken->getExpiry().toString() << ", maxUseCount = " << (authToken->getMaxUseCount() ? std::to_string(*authToken->getMaxUseCount()) : "<unset>"));
|
||||
|
||||
// TODO per domain
|
||||
if (user->getAuthTokensCount() >= 50)
|
||||
db::AuthToken::removeExpiredTokens(session, domain.str(), Wt::WDateTime::currentDateTime());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<AuthTokenService::AuthTokenInfo> AuthTokenService::processAuthToken(core::LiteralString domain, std::string_view token)
|
||||
{
|
||||
db::Session& session{ getDbSession() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
db::AuthToken::pointer authToken{ db::AuthToken::find(session, secretHash) };
|
||||
db::AuthToken::pointer authToken{ db::AuthToken::find(session, domain.str(), token) };
|
||||
if (!authToken)
|
||||
return std::nullopt;
|
||||
|
||||
if (authToken->getExpiry() < Wt::WDateTime::currentDateTime())
|
||||
if (authToken->getExpiry().isValid() && authToken->getExpiry() < Wt::WDateTime::currentDateTime())
|
||||
{
|
||||
authToken.remove();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!");
|
||||
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "' on domain '" << domain.str() << "'");
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res{ authToken->getUser()->getId(), authToken->getExpiry() };
|
||||
authToken.remove();
|
||||
AuthTokenInfo res{ createAuthTokenInfo(authToken) };
|
||||
|
||||
const std::size_t tokenUseCount{ authToken.modify()->incUseCount() };
|
||||
authToken.modify()->setLastUsed(Wt::WDateTime::currentDateTime());
|
||||
|
||||
if (auto maxUseCount{ authToken->getMaxUseCount() })
|
||||
{
|
||||
if (*maxUseCount >= tokenUseCount)
|
||||
authToken.remove();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult
|
||||
AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
|
||||
AuthTokenService::AuthTokenProcessResult AuthTokenService::processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
|
||||
{
|
||||
// Do not waste too much resource on brute force attacks (optim)
|
||||
{
|
||||
@@ -107,7 +129,7 @@ namespace lms::auth
|
||||
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Throttled };
|
||||
}
|
||||
|
||||
auto res{ processAuthToken(tokenValue) };
|
||||
auto res{ processAuthToken(domain, tokenValue) };
|
||||
{
|
||||
std::unique_lock lock{ _mutex };
|
||||
|
||||
@@ -122,22 +144,40 @@ namespace lms::auth
|
||||
|
||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||
onUserAuthenticated(res->userId);
|
||||
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, std::move(*res) };
|
||||
return AuthTokenProcessResult{ AuthTokenProcessResult::State::Granted, res };
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
AuthTokenService::clearAuthTokens(db::UserId userId)
|
||||
void AuthTokenService::visitAuthTokens(core::LiteralString domain, db::UserId userId, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor)
|
||||
{
|
||||
db::Session& session{ getDbSession() };
|
||||
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
db::User::pointer user{ db::User::find(session, userId) };
|
||||
if (!user)
|
||||
throw Exception{ "User deleted" };
|
||||
|
||||
user.modify()->clearAuthTokens();
|
||||
db::AuthToken::find(session, domain.str(), userId, [&](const db::AuthToken::pointer& authToken) {
|
||||
const AuthTokenInfo info{ createAuthTokenInfo(authToken) };
|
||||
visitor(info, authToken->getValue());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AuthTokenService::clearAuthTokens(core::LiteralString domain, db::UserId userId)
|
||||
{
|
||||
db::Session& session{ getDbSession() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
db::AuthToken::clearUserTokens(session, domain.str(), userId);
|
||||
}
|
||||
}
|
||||
|
||||
const AuthTokenService::DomainParameters& AuthTokenService::getDomainParameters(core::LiteralString domain) const
|
||||
{
|
||||
auto it{ _domainParameters.find(domain) };
|
||||
if (it == std::cend(_domainParameters))
|
||||
throw Exception{ "Invalid auth token domain" };
|
||||
|
||||
return it->second;
|
||||
}
|
||||
} // namespace lms::auth
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace lms::auth
|
||||
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
|
||||
{
|
||||
public:
|
||||
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntries);
|
||||
AuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
|
||||
|
||||
AuthTokenService(const AuthTokenService&) = delete;
|
||||
AuthTokenService& operator=(const AuthTokenService&) = delete;
|
||||
@@ -44,13 +44,17 @@ namespace lms::auth
|
||||
AuthTokenService& operator=(AuthTokenService&&) = delete;
|
||||
|
||||
private:
|
||||
AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
|
||||
std::string createAuthToken(db::UserId userId, const Wt::WDateTime& expiry) override;
|
||||
void clearAuthTokens(db::UserId userId) override;
|
||||
void registerDomain(core::LiteralString domain, const DomainParameters& params) override;
|
||||
AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
|
||||
void visitAuthTokens(core::LiteralString domain, db::UserId userId, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor) override;
|
||||
void createAuthToken(core::LiteralString domain, db::UserId userId, std::string_view token) override;
|
||||
void clearAuthTokens(core::LiteralString domain, db::UserId userId) override;
|
||||
|
||||
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> processAuthToken(std::string_view secret);
|
||||
std::optional<AuthTokenInfo> processAuthToken(core::LiteralString domain, std::string_view tokenValue);
|
||||
const DomainParameters& getDomainParameters(core::LiteralString domain) const;
|
||||
|
||||
std::shared_mutex _mutex;
|
||||
std::map<core::LiteralString, DomainParameters> _domainParameters;
|
||||
LoginThrottler _loginThrottler;
|
||||
};
|
||||
} // namespace lms::auth
|
||||
|
||||
@@ -37,23 +37,20 @@ namespace lms::auth
|
||||
{
|
||||
static const Wt::Auth::SHA1HashFunction sha1Function;
|
||||
|
||||
std::unique_ptr<IPasswordService>
|
||||
createPasswordService(std::string_view passwordAuthenticationBackend, db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount)
|
||||
{
|
||||
if (passwordAuthenticationBackend == "internal")
|
||||
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntries, authTokenService);
|
||||
if (backend == "internal")
|
||||
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntryCount);
|
||||
#ifdef LMS_SUPPORT_PAM
|
||||
else if (passwordAuthenticationBackend == "pam")
|
||||
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntries, authTokenService);
|
||||
if (backend == "PAM")
|
||||
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntryCount);
|
||||
#endif // LMS_SUPPORT_PAM
|
||||
|
||||
throw Exception{ "Authentication backend '" + std::string{ passwordAuthenticationBackend } + "' is not supported!" };
|
||||
throw Exception{ "Authentication backend '" + std::string{ backend } + "' not supported!" };
|
||||
}
|
||||
|
||||
PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
PasswordServiceBase::PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries)
|
||||
: AuthServiceBase{ db }
|
||||
, _loginThrottler{ maxThrottlerEntries }
|
||||
, _authTokenService{ authTokenService }
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -36,16 +36,13 @@ namespace lms::auth
|
||||
class PasswordServiceBase : public IPasswordService, public AuthServiceBase
|
||||
{
|
||||
public:
|
||||
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
|
||||
PasswordServiceBase(db::Db& db, std::size_t maxThrottlerEntries);
|
||||
|
||||
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(std::string_view loginName, std::string_view password) = 0;
|
||||
|
||||
@@ -55,6 +52,5 @@ namespace lms::auth
|
||||
|
||||
std::shared_mutex _mutex;
|
||||
LoginThrottler _loginThrottler;
|
||||
IAuthTokenService& _authTokenService;
|
||||
};
|
||||
} // namespace lms::auth
|
||||
|
||||
@@ -38,25 +38,25 @@ namespace lms::auth
|
||||
{
|
||||
const std::string loginName{ env.headerValue(_fieldName) };
|
||||
if (loginName.empty())
|
||||
return { CheckResult::State::Denied };
|
||||
return CheckResult{ .state = CheckResult::State::Denied, .userId = {} };
|
||||
|
||||
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
|
||||
|
||||
const db::UserId userId{ getOrCreateUser(loginName) };
|
||||
onUserAuthenticated(userId);
|
||||
return { CheckResult::State::Granted, userId };
|
||||
return CheckResult{ .state = CheckResult::State::Granted, .userId = userId };
|
||||
}
|
||||
|
||||
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
|
||||
{
|
||||
const std::string loginName{ request.headerValue(_fieldName) };
|
||||
if (loginName.empty())
|
||||
return { CheckResult::State::Denied };
|
||||
return CheckResult{ .state = CheckResult::State::Denied, .userId = {} };
|
||||
|
||||
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
|
||||
|
||||
const db::UserId userId{ getOrCreateUser(loginName) };
|
||||
onUserAuthenticated(userId);
|
||||
return { CheckResult::State::Granted, userId };
|
||||
return { .state = CheckResult::State::Granted, .userId = userId };
|
||||
}
|
||||
} // namespace lms::auth
|
||||
|
||||
@@ -25,13 +25,12 @@
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/Types.hpp"
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
: PasswordServiceBase{ db, maxThrottlerEntries, authTokenService }
|
||||
InternalPasswordService::InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries)
|
||||
: PasswordServiceBase{ db, maxThrottlerEntries }
|
||||
{
|
||||
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
|
||||
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
|
||||
@@ -114,14 +113,13 @@ namespace lms::auth
|
||||
}
|
||||
|
||||
user.modify()->setPasswordHash(passwordHash);
|
||||
getAuthTokenService().clearAuthTokens(userId);
|
||||
}
|
||||
|
||||
db::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) };
|
||||
return db::User::PasswordHash{ .salt = salt, .hash = _hashFunc.compute(std::string{ password }, salt) };
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -24,17 +24,15 @@
|
||||
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "LoginThrottler.hpp"
|
||||
#include "PasswordServiceBase.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
class IAuthTokenService;
|
||||
|
||||
class InternalPasswordService : public PasswordServiceBase
|
||||
{
|
||||
public:
|
||||
InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
|
||||
InternalPasswordService(db::Db& db, std::size_t maxThrottlerEntries);
|
||||
|
||||
private:
|
||||
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
|
||||
|
||||
@@ -19,18 +19,21 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Db;
|
||||
class User;
|
||||
} // namespace lms::db
|
||||
|
||||
namespace lms::auth
|
||||
@@ -40,7 +43,15 @@ namespace lms::auth
|
||||
public:
|
||||
virtual ~IAuthTokenService() = default;
|
||||
|
||||
// Auth Token services
|
||||
struct AuthTokenInfo
|
||||
{
|
||||
db::UserId userId;
|
||||
Wt::WDateTime expiry;
|
||||
Wt::WDateTime lastUsed; // if called by processAuthToken, value is before processing
|
||||
std::size_t useCount; // if called by processAuthToken, value is before processing
|
||||
std::optional<std::size_t> maxUseCount;
|
||||
};
|
||||
|
||||
struct AuthTokenProcessResult
|
||||
{
|
||||
enum class State
|
||||
@@ -50,22 +61,25 @@ namespace lms::auth
|
||||
Denied,
|
||||
};
|
||||
|
||||
struct AuthTokenInfo
|
||||
{
|
||||
db::UserId userId;
|
||||
Wt::WDateTime expiry;
|
||||
};
|
||||
|
||||
State state{ State::Denied };
|
||||
std::optional<AuthTokenInfo> authTokenInfo{};
|
||||
};
|
||||
|
||||
// Provided token is only accepted once
|
||||
virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
|
||||
struct DomainParameters
|
||||
{
|
||||
std::optional<std::size_t> tokenMaxUseCount;
|
||||
std::optional<std::chrono::seconds> tokenDuration;
|
||||
};
|
||||
|
||||
// Returns a one time token
|
||||
virtual std::string createAuthToken(db::UserId userid, const Wt::WDateTime& expiry) = 0;
|
||||
virtual void clearAuthTokens(db::UserId userid) = 0;
|
||||
virtual void registerDomain(core::LiteralString domain, const DomainParameters& params) = 0;
|
||||
|
||||
// Processing an auth token will make its useCount increase by 1. Token is then automatically deleted if its maxUsecount is reached
|
||||
virtual AuthTokenProcessResult processAuthToken(core::LiteralString domain, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
|
||||
|
||||
virtual void visitAuthTokens(core::LiteralString domain, db::UserId userid, std::function<void(const AuthTokenInfo& info, std::string_view token)> visitor) = 0;
|
||||
|
||||
virtual void createAuthToken(core::LiteralString domain, db::UserId userid, std::string_view token) = 0;
|
||||
virtual void clearAuthTokens(core::LiteralString domain, db::UserId userid) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount);
|
||||
|
||||
@@ -58,12 +58,12 @@ namespace lms::auth
|
||||
};
|
||||
|
||||
State state{ State::Denied };
|
||||
std::optional<db::UserId> userId{};
|
||||
db::UserId userId{};
|
||||
};
|
||||
|
||||
virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0;
|
||||
virtual CheckResult processRequest(const Wt::Http::Request& request) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName, db::Db& db);
|
||||
std::unique_ptr<IEnvService> createEnvService(std::string_view backend, db::Db& db);
|
||||
} // namespace lms::auth
|
||||
|
||||
@@ -37,8 +37,6 @@ namespace lms::db
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
class IAuthTokenService;
|
||||
|
||||
class IPasswordService
|
||||
{
|
||||
public:
|
||||
@@ -53,7 +51,7 @@ namespace lms::auth
|
||||
Throttled,
|
||||
};
|
||||
State state{ State::Denied };
|
||||
std::optional<db::UserId> userId{};
|
||||
db::UserId userId{};
|
||||
std::optional<Wt::WDateTime> expiry{};
|
||||
};
|
||||
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
|
||||
@@ -73,5 +71,5 @@ namespace lms::auth
|
||||
virtual void setPassword(db::UserId userId, std::string_view newPassword) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, db::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount);
|
||||
} // namespace lms::auth
|
||||
|
||||
@@ -30,7 +30,6 @@ add_library(lmssubsonic SHARED
|
||||
impl/SubsonicId.cpp
|
||||
impl/SubsonicResource.cpp
|
||||
impl/SubsonicResponse.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmssubsonic INTERFACE
|
||||
|
||||
@@ -27,10 +27,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
struct ClientInfo
|
||||
{
|
||||
std::string ipAddress;
|
||||
std::string name;
|
||||
std::string user;
|
||||
std::string password;
|
||||
ProtocolVersion version;
|
||||
};
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -41,7 +41,8 @@ namespace lms::api::subsonic
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters;
|
||||
db::Session& dbSession;
|
||||
const db::ObjectPtr<db::User> user;
|
||||
db::ObjectPtr<db::User> user;
|
||||
std::string clientIpAddr;
|
||||
ClientInfo clientInfo;
|
||||
ProtocolVersion serverProtocolVersion;
|
||||
ResponseFormat responseFormat;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/IEnvService.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "ParameterParsing.hpp"
|
||||
@@ -41,7 +41,6 @@
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "entrypoints/AlbumSongLists.hpp"
|
||||
#include "entrypoints/Bookmarks.hpp"
|
||||
#include "entrypoints/Browsing.hpp"
|
||||
@@ -132,9 +131,10 @@ namespace lms::api::subsonic
|
||||
return res;
|
||||
}
|
||||
|
||||
void checkUserTypeIsAllowed(RequestContext& context, core::EnumSet<db::UserType> allowedUserTypes)
|
||||
void checkUserTypeIsAllowed(const db::User::pointer& user, core::EnumSet<db::UserType> allowedUserTypes)
|
||||
{
|
||||
if (!allowedUserTypes.contains(context.user->getType()))
|
||||
assert(user);
|
||||
if (!allowedUserTypes.contains(user->getType()))
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
@@ -143,20 +143,24 @@ namespace lms::api::subsonic
|
||||
throw NotImplementedGenericError{};
|
||||
}
|
||||
|
||||
enum class AuthenticationMode
|
||||
{
|
||||
Authenticated,
|
||||
Unauthenticated,
|
||||
};
|
||||
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
|
||||
using CheckImplementedFunc = std::function<void()>;
|
||||
struct RequestEntryPointInfo
|
||||
{
|
||||
RequestHandlerFunc func;
|
||||
AuthenticationMode authMode{ AuthenticationMode::Authenticated };
|
||||
core::EnumSet<db::UserType> allowedUserTypes{ db::UserType::DEMO, db::UserType::REGULAR, db::UserType::ADMIN };
|
||||
CheckImplementedFunc checkFunc{};
|
||||
};
|
||||
|
||||
const std::unordered_map<core::LiteralString, RequestEntryPointInfo, core::LiteralStringHash, core::LiteralStringEqual> requestEntryPoints{
|
||||
// System
|
||||
{ "/ping", { handlePingRequest } },
|
||||
{ "/getLicense", { handleGetLicenseRequest } },
|
||||
{ "/getOpenSubsonicExtensions", { handleGetOpenSubsonicExtensions } },
|
||||
{ "/getOpenSubsonicExtensions", { handleGetOpenSubsonicExtensions, AuthenticationMode::Unauthenticated } },
|
||||
|
||||
// Browsing
|
||||
{ "/getMusicFolders", { handleGetMusicFoldersRequest } },
|
||||
@@ -240,11 +244,11 @@ namespace lms::api::subsonic
|
||||
|
||||
// User management
|
||||
{ "/getUser", { handleGetUserRequest } },
|
||||
{ "/getUsers", { handleGetUsersRequest, { db::UserType::ADMIN } } },
|
||||
{ "/createUser", { handleCreateUserRequest, { db::UserType::ADMIN }, &utils::checkSetPasswordImplemented } },
|
||||
{ "/updateUser", { handleUpdateUserRequest, { db::UserType::ADMIN } } },
|
||||
{ "/deleteUser", { handleDeleteUserRequest, { db::UserType::ADMIN } } },
|
||||
{ "/changePassword", { handleChangePassword, { db::UserType::REGULAR, db::UserType::ADMIN }, &utils::checkSetPasswordImplemented } },
|
||||
{ "/getUsers", { handleGetUsersRequest, AuthenticationMode::Authenticated, { db::UserType::ADMIN } } },
|
||||
{ "/createUser", { handleNotImplemented } },
|
||||
{ "/updateUser", { handleNotImplemented } },
|
||||
{ "/deleteUser", { handleNotImplemented } },
|
||||
{ "/changePassword", { handleNotImplemented } },
|
||||
|
||||
// Bookmarks
|
||||
{ "/getBookmarks", { handleGetBookmarks } },
|
||||
@@ -255,7 +259,7 @@ namespace lms::api::subsonic
|
||||
|
||||
// Media library scanning
|
||||
{ "/getScanStatus", { Scan::handleGetScanStatus } },
|
||||
{ "/startScan", { Scan::handleStartScan, { db::UserType::ADMIN } } },
|
||||
{ "/startScan", { Scan::handleStartScan, AuthenticationMode::Authenticated, { db::UserType::ADMIN } } },
|
||||
};
|
||||
|
||||
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
|
||||
@@ -278,6 +282,16 @@ namespace lms::api::subsonic
|
||||
TLSMonotonicMemoryResourceCleaner(const TLSMonotonicMemoryResourceCleaner&) = delete;
|
||||
TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete;
|
||||
};
|
||||
|
||||
db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId)
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
if (db::User::pointer user{ db::User::find(session, userId) })
|
||||
return user;
|
||||
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SubsonicResource::SubsonicResource(db::Db& db)
|
||||
@@ -317,10 +331,11 @@ namespace lms::api::subsonic
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
|
||||
|
||||
if (itEntryPoint->second.checkFunc)
|
||||
itEntryPoint->second.checkFunc();
|
||||
|
||||
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes);
|
||||
if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
|
||||
{
|
||||
requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
|
||||
checkUserTypeIsAllowed(requestContext.user, itEntryPoint->second.allowedUserTypes);
|
||||
}
|
||||
|
||||
const Response resp{ [&] {
|
||||
LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest");
|
||||
@@ -343,11 +358,19 @@ namespace lms::api::subsonic
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first);
|
||||
|
||||
// Media retrieval endpoints are always authenticated
|
||||
// Optim: no need to reauth user for each continuation
|
||||
if (!request.continuation())
|
||||
requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
|
||||
|
||||
itStreamHandler->second(requestContext, request, response);
|
||||
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
|
||||
return;
|
||||
}
|
||||
|
||||
// do not disclose unhandled commands for unauthenticated users
|
||||
authenticateUser(request);
|
||||
|
||||
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
|
||||
throw UnknownEntryPointGenericError{};
|
||||
}
|
||||
@@ -391,16 +414,9 @@ namespace lms::api::subsonic
|
||||
const auto& parameters{ request.getParameterMap() };
|
||||
ClientInfo res;
|
||||
|
||||
if (hasParameter(parameters, "t"))
|
||||
throw TokenAuthenticationNotSupportedForLDAPUsersError{};
|
||||
|
||||
res.ipAddress = request.clientAddress();
|
||||
|
||||
// Mandatory parameters
|
||||
res.name = getMandatoryParameterAs<std::string>(parameters, "c");
|
||||
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
|
||||
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
|
||||
res.password = decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(parameters, "p"));
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -409,25 +425,15 @@ namespace lms::api::subsonic
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
|
||||
const ClientInfo clientInfo{ getClientInfo(request) };
|
||||
const db::UserId userId{ authenticateUser(request, clientInfo) };
|
||||
bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) };
|
||||
bool enableDefaultCover{ _defaultReleaseCoverClients.contains(clientInfo.name) };
|
||||
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
|
||||
|
||||
db::User::pointer user;
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
user = db::User::find(session, userId);
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return RequestContext{
|
||||
.parameters = parameters,
|
||||
.dbSession = _db.getTLSSession(),
|
||||
.user = user,
|
||||
.user = db::User::pointer{},
|
||||
.clientIpAddr = request.clientAddress(),
|
||||
.clientInfo = clientInfo,
|
||||
.serverProtocolVersion = getServerProtocolVersion(clientInfo.name),
|
||||
.responseFormat = format,
|
||||
@@ -436,46 +442,49 @@ namespace lms::api::subsonic
|
||||
};
|
||||
}
|
||||
|
||||
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
|
||||
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request)
|
||||
{
|
||||
// if the request if a continuation, the user is already authenticated
|
||||
if (request.continuation())
|
||||
const auto& parameters{ request.getParameterMap() };
|
||||
|
||||
if (hasParameter(parameters, "t"))
|
||||
throw TokenAuthenticationNotSupportedForLDAPUsersError{};
|
||||
|
||||
const auto user{ getParameterAs<std::string>(parameters, "u") };
|
||||
const auto password{ getParameterAs<std::string>(parameters, "p") };
|
||||
const auto apiKey{ getParameterAs<std::string>(parameters, "apiKey") };
|
||||
|
||||
if (user && !password)
|
||||
throw RequiredParameterMissingError{ "p" };
|
||||
if (!user && password)
|
||||
throw RequiredParameterMissingError{ "u" };
|
||||
if (apiKey && password)
|
||||
throw MultipleConflictingAuthenticationMechanismsProvidedError{};
|
||||
if (!apiKey && !password)
|
||||
throw RequiredParameterMissingError{ "apiKey" };
|
||||
|
||||
const auto clientAddress{ boost::asio::ip::address::from_string(request.clientAddress()) };
|
||||
const std::string authToken{ apiKey ? *apiKey : decodePasswordIfNeeded(*password) };
|
||||
|
||||
const auto authResult{ core::Service<auth::IAuthTokenService>::get()->processAuthToken("subsonic", clientAddress, authToken) };
|
||||
switch (authResult.state)
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto user{ db::User::find(session, clientInfo.user) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
return user->getId();
|
||||
}
|
||||
|
||||
if (auto* authEnvService{ core::Service<auth::IEnvService>::get() })
|
||||
{
|
||||
const auto checkResult{ authEnvService->processRequest(request) };
|
||||
if (checkResult.state != auth::IEnvService::CheckResult::State::Granted)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
return *checkResult.userId;
|
||||
}
|
||||
else if (auto* authPasswordService{ core::Service<auth::IPasswordService>::get() })
|
||||
{
|
||||
const auto checkResult{ authPasswordService->checkUserPassword(boost::asio::ip::address::from_string(request.clientAddress()), clientInfo.user, clientInfo.password) };
|
||||
|
||||
switch (checkResult.state)
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
|
||||
if (user)
|
||||
{
|
||||
case auth::IPasswordService::CheckResult::State::Granted:
|
||||
return *checkResult.userId;
|
||||
break;
|
||||
case auth::IPasswordService::CheckResult::State::Denied:
|
||||
throw WrongUsernameOrPasswordError{};
|
||||
case auth::IPasswordService::CheckResult::State::Throttled:
|
||||
throw LoginThrottledGenericError{};
|
||||
const auto authenticatedUser{ getUserFromUserId(_db.getTLSSession(), authResult.authTokenInfo->userId) };
|
||||
if (!authenticatedUser || authenticatedUser->getLoginName() != *user)
|
||||
throw WrongUsernameOrPasswordError{};
|
||||
}
|
||||
return authResult.authTokenInfo->userId;
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
|
||||
if (apiKey)
|
||||
throw InvalidAPIkeyError{};
|
||||
else
|
||||
throw WrongUsernameOrPasswordError{};
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
|
||||
throw LoginThrottledGenericError{};
|
||||
}
|
||||
|
||||
throw InternalErrorGenericError{ "No service available to authenticate user" };
|
||||
throw InternalErrorGenericError{ "Cannot authenticate user" };
|
||||
}
|
||||
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace lms::api::subsonic
|
||||
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
||||
ClientInfo getClientInfo(const Wt::Http::Request& request);
|
||||
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
||||
db::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
||||
db::UserId authenticateUser(const Wt::Http::Request& request);
|
||||
|
||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||
const std::unordered_set<std::string> _openSubsonicDisabledClients;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
// Max count expected from all API methods that expose a count
|
||||
static inline constexpr std::size_t defaultMaxCountSize{ 1000 };
|
||||
static inline constexpr std::size_t defaultMaxCountSize{ 1'000 };
|
||||
|
||||
enum class ResponseFormat
|
||||
{
|
||||
@@ -54,6 +54,9 @@ namespace lms::api::subsonic
|
||||
ServerMustUpgrade = 30,
|
||||
WrongUsernameOrPassword = 40,
|
||||
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
||||
ProvidedAuthenticationMechanismNotSupported = 42,
|
||||
MultipleConflictingAuthenticationMechanismsProvided = 43,
|
||||
InvalidAPIkey = 44,
|
||||
UserNotAuthorized = 50,
|
||||
RequestedDataNotFound = 70,
|
||||
};
|
||||
@@ -130,6 +133,45 @@ namespace lms::api::subsonic
|
||||
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
||||
};
|
||||
|
||||
class ProvidedAuthenticationMechanismNotSupportedError : public Error
|
||||
{
|
||||
public:
|
||||
ProvidedAuthenticationMechanismNotSupportedError()
|
||||
: Error{ Code::ProvidedAuthenticationMechanismNotSupported } {}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override
|
||||
{
|
||||
return "Provided authentication mechanism not supported.";
|
||||
}
|
||||
};
|
||||
|
||||
class MultipleConflictingAuthenticationMechanismsProvidedError : public Error
|
||||
{
|
||||
public:
|
||||
MultipleConflictingAuthenticationMechanismsProvidedError()
|
||||
: Error{ Code::MultipleConflictingAuthenticationMechanismsProvided } {}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override
|
||||
{
|
||||
return "Multiple conflicting authentication mechanisms provided.";
|
||||
}
|
||||
};
|
||||
|
||||
class InvalidAPIkeyError : public Error
|
||||
{
|
||||
public:
|
||||
InvalidAPIkeyError()
|
||||
: Error{ Code::InvalidAPIkey } {}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override
|
||||
{
|
||||
return "Invalid API key.";
|
||||
}
|
||||
};
|
||||
|
||||
class UserNotAuthorizedError : public Error
|
||||
{
|
||||
public:
|
||||
@@ -153,7 +195,7 @@ namespace lms::api::subsonic
|
||||
class InternalErrorGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
InternalErrorGenericError(const std::string& message)
|
||||
InternalErrorGenericError(std::string_view message)
|
||||
: _message{ message } {}
|
||||
|
||||
private:
|
||||
@@ -176,26 +218,6 @@ namespace lms::api::subsonic
|
||||
std::string getMessage() const override { return "Unknown API method"; }
|
||||
};
|
||||
|
||||
class PasswordTooWeakGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password too weak"; }
|
||||
};
|
||||
|
||||
class PasswordMustMatchLoginNameGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password must match login name"; }
|
||||
};
|
||||
|
||||
class DemoUserCannotChangePasswordGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
||||
};
|
||||
|
||||
class UserAlreadyExistsGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "User already exists"; }
|
||||
};
|
||||
|
||||
class BadParameterGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.hpp"
|
||||
|
||||
#include "core/Service.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace lms::api::subsonic::utils
|
||||
{
|
||||
void checkSetPasswordImplemented()
|
||||
{
|
||||
auth::IPasswordService* passwordService{ core::Service<auth::IPasswordService>::get() };
|
||||
if (!passwordService || !passwordService->canSetPasswords())
|
||||
throw NotImplementedGenericError{};
|
||||
}
|
||||
|
||||
std::string makeNameFilesystemCompatible(std::string_view name)
|
||||
{
|
||||
return core::stringUtils::replaceInString(name, "/", "_");
|
||||
}
|
||||
|
||||
} // namespace lms::api::subsonic::utils
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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>
|
||||
#include <string_view>
|
||||
|
||||
namespace lms::api::subsonic::utils
|
||||
{
|
||||
void checkSetPasswordImplemented();
|
||||
std::string makeNameFilesystemCompatible(std::string_view name);
|
||||
} // namespace lms::api::subsonic::utils
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "responses/Album.hpp"
|
||||
#include "responses/AlbumInfo.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
std::string clientAddress;
|
||||
std::string clientName;
|
||||
std::string userName;
|
||||
UserId user;
|
||||
MediaLibraryId library;
|
||||
std::size_t offset{};
|
||||
auto operator<=>(const ScanInfo&) const = default;
|
||||
@@ -177,9 +177,9 @@ namespace lms::api::subsonic
|
||||
else
|
||||
{
|
||||
ScanTracker<ArtistId>::ScanInfo scanInfo{
|
||||
.clientAddress = context.clientInfo.ipAddress,
|
||||
.clientAddress = context.clientIpAddr,
|
||||
.clientName = context.clientInfo.name,
|
||||
.userName = context.clientInfo.user,
|
||||
.user = context.user->getId(),
|
||||
.library = mediaLibrary,
|
||||
.offset = artistOffset
|
||||
};
|
||||
@@ -241,9 +241,9 @@ namespace lms::api::subsonic
|
||||
else
|
||||
{
|
||||
ScanTracker<ReleaseId>::ScanInfo scanInfo{
|
||||
.clientAddress = context.clientInfo.ipAddress,
|
||||
.clientAddress = context.clientIpAddr,
|
||||
.clientName = context.clientInfo.name,
|
||||
.userName = context.clientInfo.user,
|
||||
.user = context.user->getId(),
|
||||
.library = mediaLibrary,
|
||||
.offset = albumOffset
|
||||
};
|
||||
@@ -305,9 +305,9 @@ namespace lms::api::subsonic
|
||||
else
|
||||
{
|
||||
ScanTracker<TrackId>::ScanInfo scanInfo{
|
||||
.clientAddress = context.clientInfo.ipAddress,
|
||||
.clientAddress = context.clientIpAddr,
|
||||
.clientName = context.clientInfo.name,
|
||||
.userName = context.clientInfo.user,
|
||||
.user = context.user->getId(),
|
||||
.library = mediaLibrary,
|
||||
.offset = songOffset
|
||||
};
|
||||
|
||||
@@ -41,6 +41,12 @@ namespace lms::api::subsonic
|
||||
songLyricsNode.addArrayValue("versions", 1);
|
||||
}
|
||||
|
||||
{
|
||||
Response::Node& apiKeyAuthentication{ response.createArrayNode("openSubsonicExtensions") };
|
||||
apiKeyAuthentication.setAttribute("name", "apiKeyAuthentication");
|
||||
apiKeyAuthentication.addArrayValue("versions", 1);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "responses/User.hpp"
|
||||
|
||||
namespace lms::api::subsonic
|
||||
@@ -52,150 +51,4 @@ namespace lms::api::subsonic
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleCreateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||
// Just ignore all the other fields as we don't handle them
|
||||
|
||||
db::UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (user)
|
||||
throw UserAlreadyExistsGenericError{};
|
||||
|
||||
user = context.dbSession.create<User>(username);
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
auto removeCreatedUser{ [&] {
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
User::pointer user{ User::find(context.dbSession, userId) };
|
||||
if (user)
|
||||
user.remove();
|
||||
} };
|
||||
|
||||
try
|
||||
{
|
||||
core::Service<auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const auth::PasswordTooWeakException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const auth::Exception& exception)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleDeleteUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
// cannot delete ourself
|
||||
if (user->getId() == context.user->getId())
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
user.remove();
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleUpdateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::optional<std::string> password{ getParameterAs<std::string>(context.parameters, "password") };
|
||||
|
||||
UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
if (password)
|
||||
{
|
||||
utils::checkSetPasswordImplemented();
|
||||
|
||||
try
|
||||
{
|
||||
core::Service<auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password));
|
||||
}
|
||||
catch (const auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const auth::Exception&)
|
||||
{
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleChangePassword(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||
|
||||
try
|
||||
{
|
||||
db::UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
core::Service<auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const auth::Exception& authException)
|
||||
{
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
} // namespace lms::api::subsonic
|
||||
@@ -38,7 +38,6 @@
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
#include "responses/Contributor.hpp"
|
||||
#include "responses/ItemGenre.hpp"
|
||||
|
||||
+40
-13
@@ -45,6 +45,7 @@
|
||||
#include "services/scanner/IScannerService.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "subsonic/SubsonicResource.hpp"
|
||||
#include "ui/Auth.hpp"
|
||||
#include "ui/LmsApplication.hpp"
|
||||
#include "ui/LmsApplicationManager.hpp"
|
||||
#include "ui/LmsInitApplication.hpp"
|
||||
@@ -79,6 +80,19 @@ namespace lms
|
||||
throw core::LmsException{ "Invalid config value for 'log-min-severity'" };
|
||||
}
|
||||
|
||||
ui::AuthenticationBackend getUIAuthenticationBackend()
|
||||
{
|
||||
const std::string backend{ core::stringUtils::stringToLower(core::Service<core::IConfig>::get()->getString("authentication-backend", "internal")) };
|
||||
if (backend == "internal")
|
||||
return ui::AuthenticationBackend::Internal;
|
||||
if (backend == "pam")
|
||||
return ui::AuthenticationBackend::PAM;
|
||||
if (backend == "http-headers")
|
||||
return ui::AuthenticationBackend::Env;
|
||||
|
||||
throw core::LmsException{ "Invalid config value for 'authentication-backend'" };
|
||||
}
|
||||
|
||||
std::optional<core::tracing::Level> getTracingLevel()
|
||||
{
|
||||
std::string_view tracingLevel{ core::Service<core::IConfig>::get()->getString("tracing-level", "disabled") };
|
||||
@@ -320,24 +334,37 @@ namespace lms
|
||||
|
||||
ui::LmsApplicationManager appManager;
|
||||
|
||||
const std::size_t loginThrottlerMaxEntries{ config->getULong("login-throttler-max-entries", 10'000) };
|
||||
// Service initialization order is important (reverse-order for deinit)
|
||||
core::Service<core::IChildProcessManager> childProcessManagerService{ core::createChildProcessManager(ioContext) };
|
||||
core::Service<auth::IAuthTokenService> authTokenService;
|
||||
|
||||
const ui::AuthenticationBackend uiAuthenticationBackend{ getUIAuthenticationBackend() };
|
||||
core::Service<auth::IAuthTokenService> authTokenService{ auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10'000)) };
|
||||
core::Service<auth::IPasswordService> authPasswordService;
|
||||
core::Service<auth::IEnvService> authEnvService;
|
||||
|
||||
const std::string authenticationBackend{ core::stringUtils::stringToLower(config->getString("authentication-backend", "internal")) };
|
||||
if (authenticationBackend == "internal" || authenticationBackend == "pam")
|
||||
authTokenService->registerDomain("ui", auth::IAuthTokenService::DomainParameters{
|
||||
.tokenMaxUseCount = 1,
|
||||
.tokenDuration = std::chrono::weeks{ 8 },
|
||||
});
|
||||
|
||||
authTokenService->registerDomain("subsonic", auth::IAuthTokenService::DomainParameters{
|
||||
.tokenMaxUseCount = std::nullopt, // no usage limit
|
||||
.tokenDuration = std::nullopt, // no time limit
|
||||
});
|
||||
|
||||
switch (uiAuthenticationBackend)
|
||||
{
|
||||
authTokenService.assign(auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10000)));
|
||||
authPasswordService.assign(auth::createPasswordService(authenticationBackend, database, config->getULong("login-throttler-max-entriees", 10000), *authTokenService.get()));
|
||||
case ui::AuthenticationBackend::Internal:
|
||||
authPasswordService.assign(auth::createPasswordService("internal", database, loginThrottlerMaxEntries));
|
||||
break;
|
||||
case ui::AuthenticationBackend::PAM:
|
||||
authPasswordService.assign(auth::createPasswordService("PAM", database, loginThrottlerMaxEntries));
|
||||
break;
|
||||
case ui::AuthenticationBackend::Env:
|
||||
authEnvService.assign(auth::createEnvService("http-headers", database));
|
||||
break;
|
||||
}
|
||||
else if (authenticationBackend == "http-headers")
|
||||
{
|
||||
authEnvService.assign(auth::createEnvService(authenticationBackend, database));
|
||||
}
|
||||
else
|
||||
throw core::LmsException{ "Bad value '" + authenticationBackend + "' for 'authentication-backend'" };
|
||||
|
||||
image::init(argv[0]);
|
||||
core::Service<cover::IArtworkService> artworkService{ cover::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") };
|
||||
@@ -370,8 +397,8 @@ namespace lms
|
||||
|
||||
// bind UI entry point
|
||||
server.addEntryPoint(Wt::EntryPointType::Application,
|
||||
[&](const Wt::WEnvironment& env) {
|
||||
return ui::LmsApplication::create(env, database, appManager);
|
||||
[&database, &appManager, uiAuthenticationBackend](const Wt::WEnvironment& env) {
|
||||
return ui::LmsApplication::create(env, database, appManager, uiAuthenticationBackend);
|
||||
});
|
||||
|
||||
proxyScannerEventsToApplication(*scannerService, server);
|
||||
|
||||
+32
-16
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "Auth.hpp"
|
||||
|
||||
#include <Wt/Auth/HashFunction.h>
|
||||
|
||||
#include <Wt/WCheckBox.h>
|
||||
#include <Wt/WEnvironment.h>
|
||||
#include <Wt/WFormModel.h>
|
||||
@@ -42,14 +44,18 @@ namespace lms::ui
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr core::LiteralString authTokenDomain{ "ui" };
|
||||
static const std::string authCookieName{ "LmsAuth" };
|
||||
static const std::string authCookieSalt{ Wt::Auth::SHA1HashFunction{}.compute(authCookieName, authTokenDomain.c_str()) }; // changing this will invalidate existing tokens
|
||||
|
||||
void createAuthToken(db::UserId userId, const Wt::WDateTime& expiry)
|
||||
{
|
||||
const std::string secret{ core::Service<auth::IAuthTokenService>::get()->createAuthToken(userId, expiry) };
|
||||
const std::string authCookie{ Wt::WRandom::generateId(64) };
|
||||
const std::string hashedAuthCookie{ Wt::Auth::SHA1HashFunction{}.compute(authCookie, authCookieSalt) };
|
||||
core::Service<auth::IAuthTokenService>::get()->createAuthToken(authTokenDomain, userId, hashedAuthCookie);
|
||||
|
||||
LmsApp->setCookie(authCookieName,
|
||||
secret,
|
||||
authCookie,
|
||||
expiry.toTime_t() - Wt::WDateTime::currentDateTime().toTime_t(),
|
||||
"",
|
||||
"",
|
||||
@@ -64,7 +70,8 @@ namespace lms::ui
|
||||
static const Field PasswordField;
|
||||
static const Field RememberMeField;
|
||||
|
||||
AuthModel()
|
||||
AuthModel(auth::IPasswordService& passwordService)
|
||||
: _passwordService{ passwordService }
|
||||
{
|
||||
addField(LoginNameField);
|
||||
addField(PasswordField);
|
||||
@@ -90,8 +97,7 @@ namespace lms::ui
|
||||
if (Wt::asNumber(value(RememberMeField)))
|
||||
{
|
||||
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
|
||||
|
||||
createAuthToken(*_userId, isDemo ? now.addDays(3) : now.addYears(1));
|
||||
createAuthToken(_userId, isDemo ? now.addDays(3) : now.addYears(1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,14 +107,14 @@ namespace lms::ui
|
||||
|
||||
if (field == PasswordField)
|
||||
{
|
||||
const auto checkResult{ core::Service<auth::IPasswordService>::get()->checkUserPassword(
|
||||
const auto checkResult{ _passwordService.checkUserPassword(
|
||||
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
|
||||
valueText(LoginNameField).toUTF8(),
|
||||
valueText(PasswordField).toUTF8()) };
|
||||
switch (checkResult.state)
|
||||
{
|
||||
case auth::IPasswordService::CheckResult::State::Granted:
|
||||
_userId = *checkResult.userId;
|
||||
_userId = checkResult.userId;
|
||||
break;
|
||||
case auth::IPasswordService::CheckResult::State::Denied:
|
||||
error = Wt::WString::tr("Lms.password-bad-login-combination");
|
||||
@@ -128,10 +134,11 @@ namespace lms::ui
|
||||
return (validation(field).state() == Wt::ValidationState::Valid);
|
||||
}
|
||||
|
||||
std::optional<db::UserId> getUserId() const { return _userId; }
|
||||
db::UserId getUserId() const { return _userId; }
|
||||
|
||||
private:
|
||||
std::optional<db::UserId> _userId;
|
||||
db::UserId _userId;
|
||||
auth::IPasswordService& _passwordService;
|
||||
};
|
||||
|
||||
const AuthModel::Field AuthModel::LoginNameField{ "login-name" };
|
||||
@@ -139,21 +146,24 @@ namespace lms::ui
|
||||
const AuthModel::Field AuthModel::RememberMeField{ "remember-me" };
|
||||
} // namespace
|
||||
|
||||
std::optional<db::UserId> processAuthToken(const Wt::WEnvironment& env)
|
||||
db::UserId processAuthToken(const Wt::WEnvironment& env)
|
||||
{
|
||||
const std::string* authCookie{ env.getCookie(authCookieName) };
|
||||
if (!authCookie)
|
||||
return std::nullopt;
|
||||
return db::UserId{};
|
||||
|
||||
const auto res{ core::Service<auth::IAuthTokenService>::get()->processAuthToken(boost::asio::ip::address::from_string(env.clientAddress()), *authCookie) };
|
||||
const std::string hashedCookie{ Wt::Auth::SHA1HashFunction{}.compute(*authCookie, authCookieSalt) };
|
||||
|
||||
const auto res{ core::Service<auth::IAuthTokenService>::get()->processAuthToken(authTokenDomain, boost::asio::ip::address::from_string(env.clientAddress()), hashedCookie) };
|
||||
switch (res.state)
|
||||
{
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
|
||||
LmsApp->setCookie(authCookieName, std::string{}, 0, "", "", env.urlScheme() == "https");
|
||||
return std::nullopt;
|
||||
return db::UserId{};
|
||||
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
|
||||
assert(res.authTokenInfo->maxUseCount && res.authTokenInfo->maxUseCount.value() == 1); // single-use token
|
||||
createAuthToken(res.authTokenInfo->userId, res.authTokenInfo->expiry);
|
||||
break;
|
||||
}
|
||||
@@ -161,10 +171,15 @@ namespace lms::ui
|
||||
return res.authTokenInfo->userId;
|
||||
}
|
||||
|
||||
Auth::Auth()
|
||||
void clearAuthTokens(db::UserId userId)
|
||||
{
|
||||
core::Service<auth::IAuthTokenService>::get()->clearAuthTokens(authTokenDomain, userId);
|
||||
}
|
||||
|
||||
PasswordAuth::PasswordAuth(auth::IPasswordService& passwordService)
|
||||
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Auth.template") }
|
||||
{
|
||||
auto model{ std::make_shared<AuthModel>() };
|
||||
auto model{ std::make_shared<AuthModel>(passwordService) };
|
||||
|
||||
auto processAuth = [this, model] {
|
||||
updateModel(model.get());
|
||||
@@ -172,7 +187,8 @@ namespace lms::ui
|
||||
if (model->validate())
|
||||
{
|
||||
model->saveData();
|
||||
userLoggedIn.emit(*model->getUserId());
|
||||
assert(model->getUserId().isValid());
|
||||
userLoggedIn.emit(model->getUserId());
|
||||
}
|
||||
else
|
||||
updateView(model.get());
|
||||
|
||||
+15
-5
@@ -19,20 +19,30 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
class IPasswordService;
|
||||
}
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
std::optional<db::UserId> processAuthToken(const Wt::WEnvironment& env);
|
||||
enum class AuthenticationBackend
|
||||
{
|
||||
Internal,
|
||||
Env,
|
||||
PAM,
|
||||
};
|
||||
|
||||
class Auth : public Wt::WTemplateFormView
|
||||
db::UserId processAuthToken(const Wt::WEnvironment& env);
|
||||
|
||||
class PasswordAuth : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
Auth();
|
||||
PasswordAuth(auth::IPasswordService& passwordService);
|
||||
|
||||
Wt::Signal<db::UserId> userLoggedIn;
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/artwork/IArtworkService.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/IEnvService.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
@@ -184,22 +185,9 @@ namespace lms::ui
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager)
|
||||
std::unique_ptr<Wt::WApplication> LmsApplication::create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend)
|
||||
{
|
||||
if (auto* authEnvService{ core::Service<auth::IEnvService>::get() })
|
||||
{
|
||||
const auto checkResult{ authEnvService->processEnv(env) };
|
||||
if (checkResult.state != auth::IEnvService::CheckResult::State::Granted)
|
||||
{
|
||||
LMS_LOG(UI, ERROR, "Cannot authenticate user from environment!");
|
||||
// return a blank page
|
||||
return std::make_unique<Wt::WApplication>(env);
|
||||
}
|
||||
|
||||
return std::make_unique<LmsApplication>(env, db, appManager, checkResult.userId);
|
||||
}
|
||||
|
||||
return std::make_unique<LmsApplication>(env, db, appManager);
|
||||
return std::make_unique<LmsApplication>(env, db, appManager, authBackend);
|
||||
}
|
||||
|
||||
LmsApplication* LmsApplication::instance()
|
||||
@@ -249,17 +237,15 @@ namespace lms::ui
|
||||
return _user->userLoginName;
|
||||
}
|
||||
|
||||
LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
db::Db& db,
|
||||
LmsApplicationManager& appManager,
|
||||
std::optional<db::UserId> userId)
|
||||
LmsApplication::LmsApplication(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend)
|
||||
: Wt::WApplication{ env }
|
||||
, _db{ db }
|
||||
, _appManager{ appManager }
|
||||
, _authBackend{ authBackend }
|
||||
{
|
||||
try
|
||||
{
|
||||
init(userId);
|
||||
init();
|
||||
}
|
||||
catch (LmsApplicationException& e)
|
||||
{
|
||||
@@ -275,7 +261,7 @@ namespace lms::ui
|
||||
|
||||
LmsApplication::~LmsApplication() = default;
|
||||
|
||||
void LmsApplication::init(std::optional<db::UserId> userId)
|
||||
void LmsApplication::init()
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("UI", "ApplicationInit");
|
||||
|
||||
@@ -291,24 +277,38 @@ namespace lms::ui
|
||||
// Handle Media Scanner events and other session events
|
||||
enableUpdates(true);
|
||||
|
||||
if (userId)
|
||||
onUserLoggedIn(*userId, false /* strongAuth */);
|
||||
else if (core::Service<auth::IPasswordService>::exists())
|
||||
db::UserId userId;
|
||||
switch (_authBackend)
|
||||
{
|
||||
case AuthenticationBackend::Env:
|
||||
{
|
||||
const auto checkResult{ core::Service<auth::IEnvService>::get()->processEnv(environment()) };
|
||||
if (checkResult.state != auth::IEnvService::CheckResult::State::Granted)
|
||||
{
|
||||
LMS_LOG(UI, ERROR, "Cannot authenticate user from environment!");
|
||||
throw core::LmsException{ "Cannot authenticate user from environment!" }; // Do not put details here at it may appear on the user rendered html
|
||||
}
|
||||
assert(checkResult.userId.isValid());
|
||||
userId = checkResult.userId;
|
||||
}
|
||||
break;
|
||||
|
||||
case AuthenticationBackend::Internal:
|
||||
[[fallthrough]];
|
||||
case AuthenticationBackend::PAM:
|
||||
// Try to authenticate using auth token ("remember me" checkbox), may fail
|
||||
userId = processAuthToken(environment());
|
||||
break;
|
||||
}
|
||||
|
||||
if (userId.isValid())
|
||||
onUserLoggedIn(userId, false /* strongAuth */);
|
||||
else
|
||||
processPasswordAuth();
|
||||
}
|
||||
|
||||
void LmsApplication::processPasswordAuth()
|
||||
{
|
||||
{
|
||||
std::optional<db::UserId> userId{ processAuthToken(environment()) };
|
||||
if (userId)
|
||||
{
|
||||
LMS_LOG(UI, DEBUG, "User authenticated using Auth token!");
|
||||
onUserLoggedIn(*userId, false /* strongAuth */);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If here is no account in the database, launch the first connection wizard
|
||||
bool firstConnection{};
|
||||
{
|
||||
@@ -318,17 +318,19 @@ namespace lms::ui
|
||||
|
||||
LMS_LOG(UI, DEBUG, "Creating root widget. First connection = " << firstConnection);
|
||||
|
||||
if (firstConnection && core::Service<auth::IPasswordService>::get()->canSetPasswords())
|
||||
assert(_authBackend == AuthenticationBackend::Internal || _authBackend == AuthenticationBackend::PAM);
|
||||
auth::IPasswordService& passwordService{ *core::Service<auth::IPasswordService>::get() };
|
||||
|
||||
if (firstConnection && _authBackend == AuthenticationBackend::Internal)
|
||||
{
|
||||
root()->addWidget(std::make_unique<InitWizardView>());
|
||||
}
|
||||
else
|
||||
{
|
||||
Auth* auth{ root()->addNew<Auth>() };
|
||||
auth->userLoggedIn.connect(this, [this](db::UserId userId) {
|
||||
onUserLoggedIn(userId, true /* strongAuth */);
|
||||
});
|
||||
root()->addNew<InitWizardView>(passwordService);
|
||||
return;
|
||||
}
|
||||
|
||||
PasswordAuth* auth{ root()->addNew<PasswordAuth>(passwordService) };
|
||||
auth->userLoggedIn.connect(this, [this](db::UserId userId) {
|
||||
onUserLoggedIn(userId, true /* strongAuth */);
|
||||
});
|
||||
}
|
||||
|
||||
void LmsApplication::finalize()
|
||||
@@ -360,11 +362,7 @@ namespace lms::ui
|
||||
|
||||
void LmsApplication::logoutUser()
|
||||
{
|
||||
{
|
||||
auto transaction{ getDbSession().createWriteTransaction() };
|
||||
getUser().modify()->clearAuthTokens();
|
||||
}
|
||||
|
||||
core::Service<auth::IAuthTokenService>::get()->clearAuthTokens("ui", getUserId());
|
||||
LMS_LOG(UI, INFO, "User '" << getUserLoginName() << " 'logged out");
|
||||
goHomeAndQuit();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "database/UserId.hpp"
|
||||
#include "services/scanner/ScannerEvents.hpp"
|
||||
|
||||
#include "Auth.hpp"
|
||||
#include "Notification.hpp"
|
||||
#include "admin/ScannerController.hpp"
|
||||
|
||||
@@ -53,10 +54,10 @@ namespace lms::ui
|
||||
class LmsApplication : public Wt::WApplication
|
||||
{
|
||||
public:
|
||||
LmsApplication(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, std::optional<db::UserId> userId = std::nullopt);
|
||||
LmsApplication(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend);
|
||||
~LmsApplication();
|
||||
|
||||
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager);
|
||||
static std::unique_ptr<Wt::WApplication> create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend);
|
||||
static LmsApplication* instance();
|
||||
|
||||
// Session application data
|
||||
@@ -73,6 +74,8 @@ namespace lms::ui
|
||||
// Proxified scanner events
|
||||
scanner::Events& getScannerEvents() { return _scannerEvents; }
|
||||
|
||||
AuthenticationBackend getAuthBackend() const { return _authBackend; }
|
||||
|
||||
// Utils
|
||||
void post(std::function<void()> func);
|
||||
void setTitle(const Wt::WString& title = "");
|
||||
@@ -88,7 +91,7 @@ namespace lms::ui
|
||||
Wt::Signal<>& preQuit() { return _preQuit; }
|
||||
|
||||
private:
|
||||
void init(std::optional<db::UserId> userId);
|
||||
void init();
|
||||
void processPasswordAuth();
|
||||
void handleException(LmsApplicationException& e);
|
||||
void goHomeAndQuit();
|
||||
@@ -106,6 +109,7 @@ namespace lms::ui
|
||||
db::Db& _db;
|
||||
Wt::Signal<> _preQuit;
|
||||
LmsApplicationManager& _appManager;
|
||||
const AuthenticationBackend _authBackend;
|
||||
scanner::Events _scannerEvents;
|
||||
struct UserAuthInfo
|
||||
{
|
||||
|
||||
+86
-11
@@ -33,6 +33,7 @@
|
||||
#include "core/Service.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
@@ -59,6 +60,7 @@ namespace lms::ui
|
||||
static inline const Field ReplayGainModeField{ "replaygain-mode" };
|
||||
static inline const Field ReplayGainPreAmpGainField{ "replaygain-preamp" };
|
||||
static inline const Field ReplayGainPreAmpGainIfNoInfoField{ "replaygain-preamp-no-rg-info" };
|
||||
static inline const Field SubsonicTokenField{ "subsonic-token" };
|
||||
static inline const Field SubsonicEnableTranscodingByDefault{ "subsonic-enable-transcoding-by-default" };
|
||||
static inline const Field SubsonicArtistListModeField{ "subsonic-artist-list-mode" };
|
||||
static inline const Field SubsonicTranscodingOutputFormatField{ "subsonic-transcoding-output-format" };
|
||||
@@ -76,9 +78,10 @@ namespace lms::ui
|
||||
using FeedbackBackendModel = ValueStringModel<db::FeedbackBackend>;
|
||||
using ScrobblingBackendModel = ValueStringModel<db::ScrobblingBackend>;
|
||||
|
||||
SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword)
|
||||
SettingsModel(auth::IPasswordService* authPasswordService, bool withOldPassword, auth::IAuthTokenService& authTokenService)
|
||||
: _authPasswordService{ authPasswordService }
|
||||
, _withOldPassword{ withOldPassword }
|
||||
, _authTokenService{ authTokenService }
|
||||
{
|
||||
initializeModels();
|
||||
|
||||
@@ -89,12 +92,15 @@ namespace lms::ui
|
||||
addField(ReplayGainModeField);
|
||||
addField(ReplayGainPreAmpGainField);
|
||||
addField(ReplayGainPreAmpGainIfNoInfoField);
|
||||
addField(SubsonicTokenField);
|
||||
addField(SubsonicEnableTranscodingByDefault);
|
||||
addField(SubsonicTranscodingOutputBitrateField);
|
||||
addField(SubsonicTranscodingOutputFormatField);
|
||||
addField(FeedbackBackendField);
|
||||
addField(ScrobblingBackendField);
|
||||
addField(ListenBrainzTokenField);
|
||||
|
||||
setValidator(SubsonicTokenField, createUUIDValidator());
|
||||
setValidator(ListenBrainzTokenField, createUUIDValidator());
|
||||
|
||||
if (_authPasswordService)
|
||||
@@ -102,11 +108,11 @@ namespace lms::ui
|
||||
if (_withOldPassword)
|
||||
{
|
||||
addField(PasswordOldField);
|
||||
setValidator(PasswordOldField, createPasswordCheckValidator());
|
||||
setValidator(PasswordOldField, createPasswordCheckValidator(*_authPasswordService));
|
||||
}
|
||||
|
||||
addField(PasswordField);
|
||||
setValidator(PasswordField, createPasswordStrengthValidator([] { return auth::PasswordValidationContext{ .loginName = std::string{ LmsApp->getUserLoginName() }, .userType = LmsApp->getUserType() }; }));
|
||||
setValidator(PasswordField, createPasswordStrengthValidator(*authPasswordService, [] { return auth::PasswordValidationContext{ .loginName = std::string{ LmsApp->getUserLoginName() }, .userType = LmsApp->getUserType() }; }));
|
||||
addField(PasswordConfirmField);
|
||||
}
|
||||
|
||||
@@ -173,7 +179,33 @@ namespace lms::ui
|
||||
LmsApp->getMediaPlayer().setSettings(settings);
|
||||
}
|
||||
|
||||
// Subsonic API
|
||||
{
|
||||
const std::string token{ Wt::asString(value(SubsonicTokenField)).toUTF8() };
|
||||
|
||||
if (token.empty())
|
||||
{
|
||||
_authTokenService.clearAuthTokens("subsonic", user->getId());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Consider there must be only one token
|
||||
bool hasNonMatchingToken{ false };
|
||||
bool hasMatchingToken{ false };
|
||||
_authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) {
|
||||
if (storedToken == token)
|
||||
hasMatchingToken = true;
|
||||
else
|
||||
hasNonMatchingToken = true;
|
||||
});
|
||||
|
||||
if (!hasMatchingToken || hasNonMatchingToken)
|
||||
{
|
||||
_authTokenService.clearAuthTokens("subsonic", user->getId());
|
||||
_authTokenService.createAuthToken("subsonic", user->getId(), token);
|
||||
}
|
||||
}
|
||||
|
||||
bool subsonicEnableTranscodingByDefault{ Wt::asNumber(value(SubsonicEnableTranscodingByDefault)) != 0 };
|
||||
user.modify()->setSubsonicEnableTranscodingByDefault(subsonicEnableTranscodingByDefault);
|
||||
|
||||
@@ -207,6 +239,7 @@ namespace lms::ui
|
||||
if (_authPasswordService && !valueText(PasswordField).empty())
|
||||
{
|
||||
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
|
||||
_authTokenService.clearAuthTokens("ui", user->getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +247,7 @@ namespace lms::ui
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
User::pointer user{ LmsApp->getUser() };
|
||||
const User::pointer user{ LmsApp->getUser() };
|
||||
|
||||
{
|
||||
auto artistReleaseSortMethodRow{ _artistReleaseSortMethodModel->getRowFromValue(user->getUIArtistReleaseSortMethod()) };
|
||||
@@ -251,7 +284,14 @@ namespace lms::ui
|
||||
setValue(ReplayGainPreAmpGainIfNoInfoField, settings.replayGain.preAmpGainIfNoInfo);
|
||||
}
|
||||
|
||||
// Subsonic
|
||||
{
|
||||
// Consider there is only one auth token
|
||||
_authTokenService.visitAuthTokens("subsonic", user->getId(), [&](const auth::IAuthTokenService::AuthTokenInfo&, std::string_view storedToken) {
|
||||
if (Wt::asString(value(SubsonicTokenField)).empty())
|
||||
setValue(SubsonicTokenField, Wt::WString::fromUTF8(std::string{ storedToken }));
|
||||
});
|
||||
|
||||
setValue(SubsonicEnableTranscodingByDefault, user->getSubsonicEnableTranscodingByDefault());
|
||||
|
||||
auto subsonicTranscodingOutputBitrateRow{ _transcodingOutputBitrateModel->getRowFromValue(user->getSubsonicDefaultTranscodingOutputBitrate()) };
|
||||
@@ -380,6 +420,8 @@ namespace lms::ui
|
||||
auth::IPasswordService* _authPasswordService{};
|
||||
bool _withOldPassword{};
|
||||
|
||||
auth::IAuthTokenService& _authTokenService;
|
||||
|
||||
std::shared_ptr<ArtistReleaseSortMethodModel> _artistReleaseSortMethodModel;
|
||||
std::shared_ptr<TranscodingModeModel> _transcodingModeModeModel;
|
||||
std::shared_ptr<ValueStringModel<Bitrate>> _transcodingOutputBitrateModel;
|
||||
@@ -403,8 +445,7 @@ namespace lms::ui
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void
|
||||
SettingsView::refreshView()
|
||||
void SettingsView::refreshView()
|
||||
{
|
||||
if (!wApp->internalPathMatches("/settings"))
|
||||
return;
|
||||
@@ -417,12 +458,14 @@ namespace lms::ui
|
||||
|
||||
auto t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Settings.template")) };
|
||||
|
||||
auto* authPasswordService{ core::Service<auth::IPasswordService>::get() };
|
||||
if (authPasswordService && !authPasswordService->canSetPasswords())
|
||||
authPasswordService = nullptr;
|
||||
|
||||
auto model{ std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong()) };
|
||||
auth::IPasswordService* authPasswordService{};
|
||||
if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
|
||||
{
|
||||
authPasswordService = core::Service<auth::IPasswordService>::get();
|
||||
assert(authPasswordService->canSetPasswords());
|
||||
}
|
||||
|
||||
auto model{ std::make_shared<SettingsModel>(authPasswordService, !LmsApp->isUserAuthStrong(), *core::Service<auth::IAuthTokenService>::get()) };
|
||||
if (authPasswordService)
|
||||
{
|
||||
t->setCondition("if-has-change-password", true);
|
||||
@@ -515,6 +558,30 @@ namespace lms::ui
|
||||
{
|
||||
t->setCondition("if-has-subsonic-api", core::Service<core::IConfig>::get()->getBool("api-subsonic", true));
|
||||
|
||||
auto subsonicToken{ std::make_unique<Wt::WLineEdit>() };
|
||||
Wt::WLineEdit* subsonicTokenPtr{ subsonicToken.get() };
|
||||
subsonicTokenPtr->setEchoMode(Wt::EchoMode::Password);
|
||||
subsonicTokenPtr->setReadOnly(true);
|
||||
t->setFormWidget(SettingsModel::SubsonicTokenField, std::move(subsonicToken));
|
||||
|
||||
auto subsonicTokenRegenBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.Settings.regen-token")) };
|
||||
subsonicTokenRegenBtn->clicked().connect(this, [subsonicTokenPtr] {
|
||||
subsonicTokenPtr->setValueText(Wt::WString::fromUTF8(std::string{ core::UUID::generate().getAsString() }));
|
||||
});
|
||||
t->bindWidget("subsonic-token-regen-btn", std::move(subsonicTokenRegenBtn));
|
||||
|
||||
auto subsonicTokenVisibilityBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) };
|
||||
subsonicTokenVisibilityBtn->clicked().connect(this, [subsonicTokenPtr] {
|
||||
subsonicTokenPtr->setEchoMode(subsonicTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password);
|
||||
});
|
||||
t->bindWidget("subsonic-token-visibility-btn", std::move(subsonicTokenVisibilityBtn));
|
||||
|
||||
auto subsonicTokenDelBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.template.trash-btn"), Wt::TextFormat::XHTML) };
|
||||
subsonicTokenDelBtn->clicked().connect(this, [subsonicTokenPtr] {
|
||||
subsonicTokenPtr->setValueText("");
|
||||
});
|
||||
t->bindWidget("subsonic-token-del-btn", std::move(subsonicTokenDelBtn));
|
||||
|
||||
// Enable transcoding by default
|
||||
t->setFormWidget(SettingsModel::SubsonicEnableTranscodingByDefault, std::make_unique<Wt::WCheckBox>());
|
||||
|
||||
@@ -555,7 +622,15 @@ namespace lms::ui
|
||||
// Backend settings
|
||||
{
|
||||
auto listenbrainzToken{ std::make_unique<Wt::WLineEdit>() };
|
||||
Wt::WLineEdit* listenbrainzTokenPtr{ listenbrainzToken.get() };
|
||||
listenbrainzTokenPtr->setEchoMode(Wt::EchoMode::Password);
|
||||
t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken));
|
||||
|
||||
auto listenbrainzTokenVisibilityBtn{ std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.template.toggle-visibility-btn"), Wt::TextFormat::XHTML) };
|
||||
listenbrainzTokenVisibilityBtn->clicked().connect(this, [listenbrainzTokenPtr] {
|
||||
listenbrainzTokenPtr->setEchoMode(listenbrainzTokenPtr->echoMode() == Wt::EchoMode::Password ? Wt::EchoMode::Normal : Wt::EchoMode::Password);
|
||||
});
|
||||
t->bindWidget("listenbrainz-token-visibility-btn", std::move(listenbrainzTokenVisibilityBtn));
|
||||
}
|
||||
|
||||
auto updateListenBrainzTokenField{ [=] {
|
||||
|
||||
@@ -47,15 +47,15 @@ namespace lms::ui
|
||||
static inline const Field PasswordField{ "password" };
|
||||
static inline const Field PasswordConfirmField{ "password-confirm" };
|
||||
|
||||
InitWizardModel()
|
||||
: Wt::WFormModel()
|
||||
InitWizardModel(auth::IPasswordService& passwordService)
|
||||
: _passwordService{ passwordService }
|
||||
{
|
||||
addField(AdminLoginField);
|
||||
addField(PasswordField);
|
||||
addField(PasswordConfirmField);
|
||||
|
||||
setValidator(AdminLoginField, createLoginNameValidator());
|
||||
setValidator(PasswordField, createPasswordStrengthValidator([this] { return auth::PasswordValidationContext{ valueText(AdminLoginField).toUTF8(), db::UserType::ADMIN }; }));
|
||||
setValidator(PasswordField, createPasswordStrengthValidator(passwordService, [this] { return auth::PasswordValidationContext{ valueText(AdminLoginField).toUTF8(), db::UserType::ADMIN }; }));
|
||||
validator(PasswordField)->setMandatory(true);
|
||||
setValidator(PasswordConfirmField, createMandatoryValidator());
|
||||
}
|
||||
@@ -71,7 +71,7 @@ namespace lms::ui
|
||||
|
||||
db::User::pointer user{ LmsApp->getDbSession().create<db::User>(valueText(AdminLoginField).toUTF8()) };
|
||||
user.modify()->setType(db::UserType::ADMIN);
|
||||
core::Service<auth::IPasswordService>::get()->setPassword(user->getId(), valueText(PasswordField).toUTF8());
|
||||
_passwordService.setPassword(user->getId(), valueText(PasswordField).toUTF8());
|
||||
}
|
||||
|
||||
bool validateField(Field field)
|
||||
@@ -97,12 +97,15 @@ namespace lms::ui
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
auth::IPasswordService& _passwordService;
|
||||
};
|
||||
|
||||
InitWizardView::InitWizardView()
|
||||
InitWizardView::InitWizardView(auth::IPasswordService& passwordService)
|
||||
: Wt::WTemplateFormView{ Wt::WString::tr("Lms.Admin.InitWizard.template") }
|
||||
{
|
||||
auto model = std::make_shared<InitWizardModel>();
|
||||
auto model = std::make_shared<InitWizardModel>(passwordService);
|
||||
|
||||
// AdminLogin
|
||||
{
|
||||
|
||||
@@ -21,13 +21,17 @@
|
||||
|
||||
#include <Wt/WTemplateFormView.h>
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
class IPasswordService;
|
||||
}
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
|
||||
class InitWizardView : public Wt::WTemplateFormView
|
||||
{
|
||||
public:
|
||||
InitWizardView();
|
||||
InitWizardView(auth::IPasswordService& passwordService);
|
||||
};
|
||||
|
||||
} // namespace lms::ui
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include "core/String.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
@@ -52,9 +53,10 @@ namespace lms::ui
|
||||
static inline const Field PasswordField{ "password" };
|
||||
static inline const Field DemoField{ "demo" };
|
||||
|
||||
UserModel(std::optional<UserId> userId, auth::IPasswordService* authPasswordService)
|
||||
UserModel(std::optional<UserId> userId, auth::IPasswordService* authPasswordService, auth::IAuthTokenService& authTokenService)
|
||||
: _userId{ userId }
|
||||
, _authPasswordService{ authPasswordService }
|
||||
, _authTokenService{ authTokenService }
|
||||
{
|
||||
if (!_userId)
|
||||
{
|
||||
@@ -65,7 +67,7 @@ namespace lms::ui
|
||||
if (authPasswordService)
|
||||
{
|
||||
addField(PasswordField);
|
||||
setValidator(PasswordField, createPasswordStrengthValidator([this] { return auth::PasswordValidationContext{ getLoginName(), getUserType() }; }));
|
||||
setValidator(PasswordField, createPasswordStrengthValidator(*authPasswordService, [this] { return auth::PasswordValidationContext{ getLoginName(), getUserType() }; }));
|
||||
if (!userId)
|
||||
validator(PasswordField)->setMandatory(true);
|
||||
}
|
||||
@@ -86,7 +88,10 @@ namespace lms::ui
|
||||
throw UserNotFoundException{};
|
||||
|
||||
if (_authPasswordService && !valueText(PasswordField).empty())
|
||||
{
|
||||
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
|
||||
_authTokenService.clearAuthTokens("ui", user->getId());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -103,6 +108,9 @@ namespace lms::ui
|
||||
|
||||
if (_authPasswordService)
|
||||
_authPasswordService->setPassword(user->getId(), valueText(PasswordField).toUTF8());
|
||||
|
||||
// For demo user, we create the subsonic API auth token now as we have no other mean to create it later
|
||||
core::Service<auth::IAuthTokenService>::get()->createAuthToken("subsonic", user->getId(), core::UUID::generate().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +185,7 @@ namespace lms::ui
|
||||
|
||||
std::optional<UserId> _userId;
|
||||
auth::IPasswordService* _authPasswordService{};
|
||||
auth::IAuthTokenService& _authTokenService;
|
||||
};
|
||||
|
||||
UserView::UserView()
|
||||
@@ -199,12 +208,14 @@ namespace lms::ui
|
||||
|
||||
Wt::WTemplateFormView* t{ addNew<Wt::WTemplateFormView>(Wt::WString::tr("Lms.Admin.User.template")) };
|
||||
|
||||
auto* authPasswordService{ core::Service<auth::IPasswordService>::get() };
|
||||
if (authPasswordService && !authPasswordService->canSetPasswords())
|
||||
authPasswordService = nullptr;
|
||||
|
||||
auto model{ std::make_shared<UserModel>(userId, authPasswordService) };
|
||||
auth::IPasswordService* authPasswordService{};
|
||||
if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
|
||||
{
|
||||
authPasswordService = core::Service<auth::IPasswordService>::get();
|
||||
assert(authPasswordService->canSetPasswords());
|
||||
}
|
||||
|
||||
auto model{ std::make_shared<UserModel>(userId, authPasswordService, *core::Service<auth::IAuthTokenService>::get()) };
|
||||
if (userId)
|
||||
{
|
||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace lms::ui
|
||||
|
||||
_container = bindNew<Wt::WContainerWidget>("users");
|
||||
|
||||
if (core::Service<auth::IPasswordService>::get() && core::Service<auth::IPasswordService>::get()->canSetPasswords())
|
||||
if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal)
|
||||
{
|
||||
setCondition("if-can-create-user", true);
|
||||
|
||||
|
||||
@@ -33,8 +33,9 @@ namespace lms::ui
|
||||
class PasswordStrengthValidator : public Wt::WValidator
|
||||
{
|
||||
public:
|
||||
PasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc)
|
||||
PasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc)
|
||||
: _passwordValidationContextGetFunc{ std::move(passwordValidationContextGetFunc) }
|
||||
, _passwordService{ passwordService }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -43,6 +44,7 @@ namespace lms::ui
|
||||
std::string javaScriptValidate() const override { return {}; }
|
||||
|
||||
PasswordValidationContextGetFunc _passwordValidationContextGetFunc;
|
||||
const auth::IPasswordService& _passwordService;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -53,7 +55,7 @@ namespace lms::ui
|
||||
|
||||
const auth::PasswordValidationContext context{ _passwordValidationContextGetFunc() };
|
||||
|
||||
switch (core::Service<auth::IPasswordService>::get()->checkPasswordAcceptability(input.toUTF8(), context))
|
||||
switch (_passwordService.checkPasswordAcceptability(input.toUTF8(), context))
|
||||
{
|
||||
case auth::IPasswordService::PasswordAcceptabilityResult::OK:
|
||||
return Wt::WValidator::Result{ Wt::ValidationState::Valid };
|
||||
@@ -66,16 +68,24 @@ namespace lms::ui
|
||||
throw core::LmsException{ "internal error" };
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc)
|
||||
std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc)
|
||||
{
|
||||
return std::make_unique<PasswordStrengthValidator>(std::move(passwordValidationContextGetFunc));
|
||||
return std::make_unique<PasswordStrengthValidator>(passwordService, std::move(passwordValidationContextGetFunc));
|
||||
}
|
||||
|
||||
class PasswordCheckValidator : public Wt::WValidator
|
||||
{
|
||||
public:
|
||||
PasswordCheckValidator(auth::IPasswordService& passwordService)
|
||||
: _passwordService{ passwordService }
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
Wt::WValidator::Result validate(const Wt::WString& input) const override;
|
||||
std::string javaScriptValidate() const override { return {}; }
|
||||
|
||||
auth::IPasswordService& _passwordService;
|
||||
};
|
||||
|
||||
Wt::WValidator::Result PasswordCheckValidator::validate(const Wt::WString& input) const
|
||||
@@ -83,7 +93,7 @@ namespace lms::ui
|
||||
if (input.empty())
|
||||
return Wt::WValidator::validate(input);
|
||||
|
||||
const auto checkResult{ core::Service<auth::IPasswordService>::get()->checkUserPassword(
|
||||
const auto checkResult{ _passwordService.checkUserPassword(
|
||||
boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()),
|
||||
LmsApp->getUserLoginName(),
|
||||
input.toUTF8()) };
|
||||
@@ -100,9 +110,9 @@ namespace lms::ui
|
||||
throw core::LmsException{ "InternalError" };
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WValidator> createPasswordCheckValidator()
|
||||
std::unique_ptr<Wt::WValidator> createPasswordCheckValidator(auth::IPasswordService& passwordService)
|
||||
{
|
||||
return std::make_unique<PasswordCheckValidator>();
|
||||
return std::make_unique<PasswordCheckValidator>(passwordService);
|
||||
}
|
||||
|
||||
} // namespace lms::ui
|
||||
@@ -25,11 +25,16 @@
|
||||
|
||||
#include "services/auth/Types.hpp"
|
||||
|
||||
namespace lms::auth
|
||||
{
|
||||
class IPasswordService;
|
||||
}
|
||||
|
||||
namespace lms::ui
|
||||
{
|
||||
using PasswordValidationContextGetFunc = std::function<auth::PasswordValidationContext()>;
|
||||
std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc);
|
||||
std::unique_ptr<Wt::WValidator> createPasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc);
|
||||
|
||||
// Check current user password
|
||||
std::unique_ptr<Wt::WValidator> createPasswordCheckValidator();
|
||||
std::unique_ptr<Wt::WValidator> createPasswordCheckValidator(auth::IPasswordService& passwordService);
|
||||
} // namespace lms::ui
|
||||
|
||||
Reference in New Issue
Block a user