OpenSubsonic API: added apiKey support, ref #544

This commit is contained in:
emeric
2024-11-24 15:23:14 +01:00
parent 0a320a8b87
commit 360623c569
54 changed files with 870 additions and 623 deletions
+4 -3
View File
@@ -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(); }
+1
View File
@@ -5,6 +5,7 @@ add_executable(test-core
LiteralString.cpp
Path.cpp
RecursiveSharedMutex.cpp
Service.cpp
String.cpp
TraceLogger.cpp
Utils.cpp
+69
View File
@@ -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
+49 -13
View File
@@ -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
+17 -1
View File
@@ -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{};
+3 -3
View File
@@ -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)");
-6
View File
@@ -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; }
+49
View File
@@ -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
View File
@@ -1,5 +1,6 @@
add_executable(test-database
AuthToken.cpp
Artist.cpp
Cluster.cpp
Common.cpp
+2
View File
@@ -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
-1
View File
@@ -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
-3
View File
@@ -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
+2 -1
View File
@@ -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;
+80 -71
View File
@@ -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
+1 -1
View File
@@ -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;
+44 -22
View File
@@ -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:
-42
View File
@@ -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
-29
View File
@@ -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"