diff --git a/approot/messages.xml b/approot/messages.xml index e34e36cf..ffee2275 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -289,7 +289,7 @@ Method to sort artist's albums Audio -These audio settings are local to your browser! +These audio settings are local to your browser Internal ListenBrainz ListenBrainz API token @@ -307,6 +307,7 @@ Original Release Date (Descending) Bad password Old password must be filled in +Generate ReplayGain mode No ReplayGain Auto @@ -317,13 +318,19 @@ Scrobbling Services Settings +New settings saved! Artist list mode All artists Album artists Track artists Subsonic API -User Interface +OpenSubsonic API key +Use this API key as the password for clients that do not support the 'API Key Authentication' extension Transcoding +Enable transcoding +Always +Only when format is not supported by the browser +Never Transcoding output bitrate Transcoding output format Matroska/Opus @@ -331,10 +338,6 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Enable transcoding -Always -Only when format is not supported by the browser -Never -New settings saved! +User Interface diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 99291866..4a7e576c 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -289,7 +289,7 @@ Méthode pour trier les albums de l'artiste Audio -Ces paramètres audio sont locaux à votre navigateur ! +Ces paramètres audio sont locaux à votre navigateur Interne ListenBrainz Jeton d'API ListenBrainz @@ -307,6 +307,7 @@ Date de sortie originale (Décroissant) Mauvais mot de passe L'ancien mot de passe doit être renseigné +Regénérer Mode ReplayGain Pas de ReplayGain Automatique @@ -317,13 +318,19 @@ Scrobbling Services Paramètres +Paramètres sauvegardés ! Mode de listage des artistes Tous les artistes Tous les artistes d'album Tous les artistes de piste API Subsonic -Interface utilisateur +Clef pour l'API OpenSubsonic +Utilisez cette clé d'API comme mot de passe pour les clients qui ne prennent pas en charge l'extension 'API Key Authentication' Transcodage +Appliquer le transcodage +Toujours +Seulement lorsque le format n'est pas pris en charge par votre navigateur +Jamais Bitrate du transcodage Format du transcodage Matroska/Opus @@ -331,11 +338,7 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Appliquer le transcodage -Toujours -Seulement lorsque le format n'est pas pris en charge par votre navigateur -Jamais -Paramètres sauvegardés ! +Interface utilisateur moins d'une seconde diff --git a/approot/messages_it.xml b/approot/messages_it.xml index 749ca015..478c0dc4 100644 --- a/approot/messages_it.xml +++ b/approot/messages_it.xml @@ -46,7 +46,7 @@ C'è stato un errore! Ritorna alla home Album non trovato - +Playlist non trovata Non sei autorizzato ad eseguire questa operazione Utente non trovato @@ -289,7 +289,7 @@ Metodo per ordinare gli album dell'artista Audio -Queste impostazioni sono salvate localmente su questo dispositivo! +Queste impostazioni sono salvate localmente su questo dispositivo Interno ListenBrainz Token API ListenBrainz @@ -307,6 +307,7 @@ Data di uscita originale (Decrescente) Password errata Devi inserire la password attuale +Genera Modalità ReplayGain No ReplayGain Automatica @@ -317,13 +318,19 @@ Scrobbling Servizi Impostazioni +Nuove impostazioni salvate! Modalità di elencazione artisti Tutti gli artisti Artisti album Artisti tracce -Subsonic API -Interfaccia utente +API Subsonic +Chiave API OpenSubsonic +Utilizza questa chiave API come password per i client che non supportano l'estensione 'Autenticazione con chiave API' Transcodifica +Abilita transcodifica +Sempre +Quando il formato non è supportato +Mai Bitrate transcodifica Formato di transcodifica Matroska/Opus @@ -331,11 +338,7 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Abilita transcodifica -Sempre -Quando il formato non è supportato -Mai -Nuove impostazioni salvate! +Interfaccia utente meno di un secondo diff --git a/approot/messages_pl.xml b/approot/messages_pl.xml index ae9bf9c4..b0945b64 100644 --- a/approot/messages_pl.xml +++ b/approot/messages_pl.xml @@ -315,7 +315,7 @@ Metoda sortowania albumów artysty Dźwięk -Te ustawienia dotyczą wyłącznie tej przeglądarki! +Te ustawienia dotyczą wyłącznie tej przeglądarki Wewnętrzny ListenBrainz ListenBrainz API token @@ -333,6 +333,7 @@ Oryginalna data wydania (Malejąco) Błędne hasło Należy podać stare hasło +Generuj Tryb ReplayGain Bez ReplayGain Auto @@ -343,13 +344,19 @@ Scrobbling Usługi Ustawienia +Ustawienia zapisane! Tryb listy artystów Wszyscy artyści Artyści z albumów Artyści ze ścieżek -Subsonic API -Interfejs użytkownika +API Subsonic +Klucz API OpenSubsonic +Użyj tego klucza API jako hasła dla klientów, którzy nie obsługują rozszerzenia 'Uwierzytelnianie za pomocą klucza API' Transkodowanie +Aktywuj transkodowanie +Zawsze +Tylko gdy przeglądarka nie wspiera formatu +Nigdy Przepływność transkodowania Format wyjściowy transkodowania Matroska/Opus @@ -357,11 +364,7 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -Aktywuj transkodowanie -Zawsze -Tylko gdy przeglądarka nie wspiera formatu -Nigdy -Ustawienia zapisane! +Interfejs użytkownika mniej niż sekunda diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml index 7b44aa1e..1a127517 100644 --- a/approot/messages_zh.xml +++ b/approot/messages_zh.xml @@ -289,7 +289,7 @@ 音频 -这些音频设置仅保存在您的本地浏览器! +这些音频设置仅保存在您的本地浏览器 @@ -307,6 +307,7 @@ 无效密码 必须填写旧密码 + ReplayGain 模式 没有 ReplayGain 自动模式 @@ -317,13 +318,19 @@ +新设置已保存! 歌手列表模式 所有歌手 专辑歌手 音轨歌手 Subsonic 应用程序接口 + 转码 +启动转码 +总是 +仅当格式不支持时使用 +从不 转码比特率 转码格式 Matroska/Opus @@ -331,11 +338,6 @@ Ogg/Opus Ogg/Vorbis WebM/Vorbis -启动转码 -总是 -仅当格式不支持时使用 -从不 -新设置已保存! diff --git a/approot/misc.xml b/approot/misc.xml index 528c6685..78ebf5b2 100644 --- a/approot/misc.xml +++ b/approot/misc.xml @@ -19,5 +19,6 @@ + diff --git a/approot/settings.xml b/approot/settings.xml index 1ee166e7..d197b3f3 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -85,6 +85,23 @@ ${} ${tr:Lms.Settings.subsonic-api} +
+ +
+ ${subsonic-token-regen-btn class="btn btn-outline-secondary"} + ${subsonic-token-del-btn class="btn btn-outline-warning"} + ${subsonic-token class="form-control"} + ${subsonic-token-visibility-btn class="btn btn-outline-secondary"} +
+ ${subsonic-token-info class="help-block"} +
+
+
${subsonic-enable-transcoding-by-default class="form-check-input"} @@ -150,9 +167,12 @@ - ${listenbrainz-token class="form-control"} -
- ${listenbrainz-token-info class="help-block"} +
+ ${listenbrainz-token class="form-control"} + ${listenbrainz-token-visibility-btn class="btn btn-outline-secondary"} +
+ ${listenbrainz-token-info class="help-block"} +
${} diff --git a/src/libs/core/include/core/Service.hpp b/src/libs/core/include/core/Service.hpp index b6603fc2..517665fb 100644 --- a/src/libs/core/include/core/Service.hpp +++ b/src/libs/core/include/core/Service.hpp @@ -24,7 +24,8 @@ namespace lms::core { - template + // Tag can be used if you have multiple services sharing the same interface + template class Service { public: @@ -46,12 +47,12 @@ namespace lms::core Class* operator->() const { - return Service::get(); + return Service::get(); } Class& operator*() const { - return *Service::get(); + return *Service::get(); } static Class* get() { return _service.get(); } diff --git a/src/libs/core/test/CMakeLists.txt b/src/libs/core/test/CMakeLists.txt index a063da46..7b6aced5 100644 --- a/src/libs/core/test/CMakeLists.txt +++ b/src/libs/core/test/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(test-core LiteralString.cpp Path.cpp RecursiveSharedMutex.cpp + Service.cpp String.cpp TraceLogger.cpp Utils.cpp diff --git a/src/libs/core/test/Service.cpp b/src/libs/core/test/Service.cpp new file mode 100644 index 00000000..0f648f5a --- /dev/null +++ b/src/libs/core/test/Service.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 . + */ + +#include + +#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().exists()); + EXPECT_EQ(Service().get(), nullptr); + + Service myService{ std::make_unique() }; + + EXPECT_TRUE(Service().exists()); + EXPECT_EQ(Service().get(), myService.get()); + } + + TEST(Service, tags) + { + Service myService{ std::make_unique() }; + Service myOtherService{ std::make_unique() }; + + EXPECT_FALSE(Service().exists()); + EXPECT_EQ(Service().get(), nullptr); + + EXPECT_TRUE((Service().exists())); + EXPECT_TRUE((Service().exists())); + EXPECT_EQ((Service().get()), myService.get()); + EXPECT_EQ((Service().get()), myOtherService.get()); + } +} // namespace lms::core::tests \ No newline at end of file diff --git a/src/libs/database/impl/AuthToken.cpp b/src/libs/database/impl/AuthToken.cpp index 5dea83c0..d963c653 100644 --- a/src/libs/database/impl/AuthToken.cpp +++ b/src/libs/database/impl/AuthToken.cpp @@ -30,29 +30,65 @@ namespace lms::db { - AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user) - : _value{ value } + AuthToken::AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional maxUseCount, ObjectPtr 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) + AuthToken::pointer AuthToken::create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional maxUseCount, ObjectPtr user) { - return session.getDboSession()->add(std::unique_ptr{ new AuthToken{ value, expiry, user } }); + return session.getDboSession()->add(std::unique_ptr{ 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().where("value = ?").bind(value)); + return utils::fetchQuerySingleResult(session.getDboSession()->query("SELECT COUNT(*) FROM auth_token")); + } + + AuthToken::pointer AuthToken::find(Session& session, AuthTokenId id) + { + return utils::fetchQuerySingleResult(session.getDboSession()->query>("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() }; + 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 visitor) + { + session.checkReadTransaction(); + + auto query{ session.getDboSession()->find() }; + 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 diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp index f8c419fd..911dedd6 100644 --- a/src/libs/database/impl/Migration.cpp +++ b/src/libs/database/impl/Migration.cpp @@ -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{}; diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index afd1b89a..d03389e2 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -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)"); diff --git a/src/libs/database/impl/User.cpp b/src/libs/database/impl/User.cpp index ecb11727..ef852783 100644 --- a/src/libs/database/impl/User.cpp +++ b/src/libs/database/impl/User.cpp @@ -97,10 +97,4 @@ namespace lms::db assert(isAudioBitrateAllowed(bitrate)); _subsonicDefaultTranscodingOutputBitrate = bitrate; } - - void User::clearAuthTokens() - { - _authTokens.clear(); - } - } // namespace lms::db diff --git a/src/libs/database/include/database/AuthToken.hpp b/src/libs/database/include/database/AuthToken.hpp index 474f0386..720716aa 100644 --- a/src/libs/database/include/database/AuthToken.hpp +++ b/src/libs/database/include/database/AuthToken.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -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 { 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 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 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 getMaxUseCount() const { return _maxUseCount; } + + // Setters + std::size_t incUseCount() { return ++_useCount; } + void setLastUsed(const Wt::WDateTime& lastUsed) { _lastUsed = lastUsed; } template 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); - static pointer create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr user); + AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional maxUseCount, ObjectPtr user); + static pointer create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional maxUseCount, ObjectPtr user); + std::string _domain; std::string _value; Wt::WDateTime _expiry; + long _useCount{}; + Wt::WDateTime _lastUsed; + std::optional _maxUseCount; Wt::Dbo::ptr _user; }; -} // namespace lms::db +} // namespace lms::db \ No newline at end of file diff --git a/src/libs/database/include/database/User.hpp b/src/libs/database/include/database/User.hpp index cbf77810..24d4a1b7 100644 --- a/src/libs/database/include/database/User.hpp +++ b/src/libs/database/include/database/User.hpp @@ -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; } diff --git a/src/libs/database/test/AuthToken.cpp b/src/libs/database/test/AuthToken.cpp new file mode 100644 index 00000000..3cf6fc91 --- /dev/null +++ b/src/libs/database/test/AuthToken.cpp @@ -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 . + */ + +#include "Common.hpp" + +#include "database/AuthToken.hpp" + +namespace lms::db::tests +{ + using ScopedAuthToken = ScopedEntity; + + 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 \ No newline at end of file diff --git a/src/libs/database/test/CMakeLists.txt b/src/libs/database/test/CMakeLists.txt index 25d651ff..60ea71d4 100644 --- a/src/libs/database/test/CMakeLists.txt +++ b/src/libs/database/test/CMakeLists.txt @@ -1,5 +1,6 @@ add_executable(test-database + AuthToken.cpp Artist.cpp Cluster.cpp Common.cpp diff --git a/src/libs/database/test/Migration.cpp b/src/libs/database/test/Migration.cpp index 9aecf7aa..8a00ec61 100644 --- a/src/libs/database/test/Migration.cpp +++ b/src/libs/database/test/Migration.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{})); diff --git a/src/libs/services/auth/impl/AuthTokenService.cpp b/src/libs/services/auth/impl/AuthTokenService.cpp index 85196f72..34055746 100644 --- a/src/libs/services/auth/impl/AuthTokenService.cpp +++ b/src/libs/services/auth/impl/AuthTokenService.cpp @@ -20,7 +20,6 @@ #include "AuthTokenService.hpp" #include -#include #include #include "core/Exception.hpp" @@ -32,72 +31,95 @@ namespace lms::auth { - - std::unique_ptr createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntries) + namespace { - return std::make_unique(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 createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount) + { + return std::make_unique(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(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::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(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(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()) : "")); + + // TODO per domain + if (user->getAuthTokensCount() >= 50) + db::AuthToken::removeExpiredTokens(session, domain.str(), Wt::WDateTime::currentDateTime()); + } + } + + std::optional 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 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 diff --git a/src/libs/services/auth/impl/AuthTokenService.hpp b/src/libs/services/auth/impl/AuthTokenService.hpp index f3160aba..eca60e47 100644 --- a/src/libs/services/auth/impl/AuthTokenService.hpp +++ b/src/libs/services/auth/impl/AuthTokenService.hpp @@ -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 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 processAuthToken(std::string_view secret); + std::optional processAuthToken(core::LiteralString domain, std::string_view tokenValue); + const DomainParameters& getDomainParameters(core::LiteralString domain) const; std::shared_mutex _mutex; + std::map _domainParameters; LoginThrottler _loginThrottler; }; } // namespace lms::auth diff --git a/src/libs/services/auth/impl/PasswordServiceBase.cpp b/src/libs/services/auth/impl/PasswordServiceBase.cpp index 4628e845..d8afa0a4 100644 --- a/src/libs/services/auth/impl/PasswordServiceBase.cpp +++ b/src/libs/services/auth/impl/PasswordServiceBase.cpp @@ -37,23 +37,20 @@ namespace lms::auth { static const Wt::Auth::SHA1HashFunction sha1Function; - std::unique_ptr - createPasswordService(std::string_view passwordAuthenticationBackend, db::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService) + std::unique_ptr createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount) { - if (passwordAuthenticationBackend == "internal") - return std::make_unique(db, maxThrottlerEntries, authTokenService); + if (backend == "internal") + return std::make_unique(db, maxThrottlerEntryCount); #ifdef LMS_SUPPORT_PAM - else if (passwordAuthenticationBackend == "pam") - return std::make_unique(db, maxThrottlerEntries, authTokenService); + if (backend == "PAM") + return std::make_unique(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 } { } diff --git a/src/libs/services/auth/impl/PasswordServiceBase.hpp b/src/libs/services/auth/impl/PasswordServiceBase.hpp index 1c32f5b9..ac75290f 100644 --- a/src/libs/services/auth/impl/PasswordServiceBase.hpp +++ b/src/libs/services/auth/impl/PasswordServiceBase.hpp @@ -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 diff --git a/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.cpp b/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.cpp index ae0c7b85..9c065e7f 100644 --- a/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.cpp +++ b/src/libs/services/auth/impl/http-headers/HttpHeadersEnvService.cpp @@ -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 diff --git a/src/libs/services/auth/impl/internal/InternalPasswordService.cpp b/src/libs/services/auth/impl/internal/InternalPasswordService.cpp index 47b9fefe..93e9ed17 100644 --- a/src/libs/services/auth/impl/internal/InternalPasswordService.cpp +++ b/src/libs/services/auth/impl/internal/InternalPasswordService.cpp @@ -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 diff --git a/src/libs/services/auth/impl/internal/InternalPasswordService.hpp b/src/libs/services/auth/impl/internal/InternalPasswordService.hpp index 9c3e07d4..17ccbf6f 100644 --- a/src/libs/services/auth/impl/internal/InternalPasswordService.hpp +++ b/src/libs/services/auth/impl/internal/InternalPasswordService.hpp @@ -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; diff --git a/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp index f18d5244..e3fea311 100644 --- a/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp +++ b/src/libs/services/auth/include/services/auth/IAuthTokenService.hpp @@ -19,18 +19,21 @@ #pragma once -#include -#include +#include +#include #include #include #include +#include +#include + +#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 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{}; }; - // Provided token is only accepted once - virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0; + struct DomainParameters + { + std::optional tokenMaxUseCount; + std::optional 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 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 createAuthTokenService(db::Db& db, std::size_t maxThrottlerEntryCount); diff --git a/src/libs/services/auth/include/services/auth/IEnvService.hpp b/src/libs/services/auth/include/services/auth/IEnvService.hpp index 6dd5d85b..64f1a41c 100644 --- a/src/libs/services/auth/include/services/auth/IEnvService.hpp +++ b/src/libs/services/auth/include/services/auth/IEnvService.hpp @@ -58,12 +58,12 @@ namespace lms::auth }; State state{ State::Denied }; - std::optional userId{}; + db::UserId userId{}; }; virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0; virtual CheckResult processRequest(const Wt::Http::Request& request) = 0; }; - std::unique_ptr createEnvService(std::string_view backendName, db::Db& db); + std::unique_ptr createEnvService(std::string_view backend, db::Db& db); } // namespace lms::auth diff --git a/src/libs/services/auth/include/services/auth/IPasswordService.hpp b/src/libs/services/auth/include/services/auth/IPasswordService.hpp index 21b12fab..065fcd50 100644 --- a/src/libs/services/auth/include/services/auth/IPasswordService.hpp +++ b/src/libs/services/auth/include/services/auth/IPasswordService.hpp @@ -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 userId{}; + db::UserId userId{}; std::optional 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 createPasswordService(std::string_view authPasswordBackend, db::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService); + std::unique_ptr createPasswordService(std::string_view backend, db::Db& db, std::size_t maxThrottlerEntryCount); } // namespace lms::auth diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index fdc51c3b..a6ad493c 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -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 diff --git a/src/libs/subsonic/impl/ClientInfo.hpp b/src/libs/subsonic/impl/ClientInfo.hpp index 1cca28bb..bd684f47 100644 --- a/src/libs/subsonic/impl/ClientInfo.hpp +++ b/src/libs/subsonic/impl/ClientInfo.hpp @@ -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 diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index e1f6f687..f129a518 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -41,7 +41,8 @@ namespace lms::api::subsonic { const Wt::Http::ParameterMap& parameters; db::Session& dbSession; - const db::ObjectPtr user; + db::ObjectPtr user; + std::string clientIpAddr; ClientInfo clientInfo; ProtocolVersion serverProtocolVersion; ResponseFormat responseFormat; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 6c66409b..40ae3f9e 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -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 allowedUserTypes) + void checkUserTypeIsAllowed(const db::User::pointer& user, core::EnumSet 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; - using CheckImplementedFunc = std::function; struct RequestEntryPointInfo { RequestHandlerFunc func; + AuthenticationMode authMode{ AuthenticationMode::Authenticated }; core::EnumSet allowedUserTypes{ db::UserType::DEMO, db::UserType::REGULAR, db::UserType::ADMIN }; - CheckImplementedFunc checkFunc{}; }; const std::unordered_map 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; @@ -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(parameters, "c"); res.version = getMandatoryParameterAs(parameters, "v"); - res.user = getMandatoryParameterAs(parameters, "u"); - res.password = decodePasswordIfNeeded(getMandatoryParameterAs(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(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(parameters, "u") }; + const auto password{ getParameterAs(parameters, "p") }; + const auto apiKey{ getParameterAs(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::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::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::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 diff --git a/src/libs/subsonic/impl/SubsonicResource.hpp b/src/libs/subsonic/impl/SubsonicResource.hpp index 9a00a448..194db761 100644 --- a/src/libs/subsonic/impl/SubsonicResource.hpp +++ b/src/libs/subsonic/impl/SubsonicResource.hpp @@ -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 _serverProtocolVersionsByClient; const std::unordered_set _openSubsonicDisabledClients; diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index b8cacdb6..9b106f6f 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -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: diff --git a/src/libs/subsonic/impl/Utils.cpp b/src/libs/subsonic/impl/Utils.cpp deleted file mode 100644 index f7746a53..00000000 --- a/src/libs/subsonic/impl/Utils.cpp +++ /dev/null @@ -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 . - */ - -#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::get() }; - if (!passwordService || !passwordService->canSetPasswords()) - throw NotImplementedGenericError{}; - } - - std::string makeNameFilesystemCompatible(std::string_view name) - { - return core::stringUtils::replaceInString(name, "/", "_"); - } - -} // namespace lms::api::subsonic::utils \ No newline at end of file diff --git a/src/libs/subsonic/impl/Utils.hpp b/src/libs/subsonic/impl/Utils.hpp deleted file mode 100644 index 4aff2a80..00000000 --- a/src/libs/subsonic/impl/Utils.hpp +++ /dev/null @@ -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 . - */ - -#pragma once - -#include -#include - -namespace lms::api::subsonic::utils -{ - void checkSetPasswordImplemented(); - std::string makeNameFilesystemCompatible(std::string_view name); -} // namespace lms::api::subsonic::utils \ No newline at end of file diff --git a/src/libs/subsonic/impl/entrypoints/Browsing.cpp b/src/libs/subsonic/impl/entrypoints/Browsing.cpp index 370e0b7c..453262ff 100644 --- a/src/libs/subsonic/impl/entrypoints/Browsing.cpp +++ b/src/libs/subsonic/impl/entrypoints/Browsing.cpp @@ -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" diff --git a/src/libs/subsonic/impl/entrypoints/Searching.cpp b/src/libs/subsonic/impl/entrypoints/Searching.cpp index fb9334cf..5a6dac99 100644 --- a/src/libs/subsonic/impl/entrypoints/Searching.cpp +++ b/src/libs/subsonic/impl/entrypoints/Searching.cpp @@ -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::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::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::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 }; diff --git a/src/libs/subsonic/impl/entrypoints/System.cpp b/src/libs/subsonic/impl/entrypoints/System.cpp index 76ad01ca..20df9d75 100644 --- a/src/libs/subsonic/impl/entrypoints/System.cpp +++ b/src/libs/subsonic/impl/entrypoints/System.cpp @@ -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 diff --git a/src/libs/subsonic/impl/entrypoints/UserManagement.cpp b/src/libs/subsonic/impl/entrypoints/UserManagement.cpp index 3858f7b1..9f47adaf 100644 --- a/src/libs/subsonic/impl/entrypoints/UserManagement.cpp +++ b/src/libs/subsonic/impl/entrypoints/UserManagement.cpp @@ -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(context.parameters, "username") }; - std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs(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(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::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(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(context.parameters, "username") }; - std::optional password{ getParameterAs(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()->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(context.parameters, "username") }; - std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs(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::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 \ No newline at end of file diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index acc4975f..93a60863 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -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" diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 6c349966..c0ce07c3 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -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::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 getTracingLevel() { std::string_view tracingLevel{ core::Service::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 childProcessManagerService{ core::createChildProcessManager(ioContext) }; - core::Service authTokenService; + + const ui::AuthenticationBackend uiAuthenticationBackend{ getUIAuthenticationBackend() }; + core::Service authTokenService{ auth::createAuthTokenService(database, config->getULong("login-throttler-max-entriees", 10'000)) }; core::Service authPasswordService; core::Service 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 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); diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index 9e744802..eb021da3 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -19,6 +19,8 @@ #include "Auth.hpp" +#include + #include #include #include @@ -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::get()->createAuthToken(userId, expiry) }; + const std::string authCookie{ Wt::WRandom::generateId(64) }; + const std::string hashedAuthCookie{ Wt::Auth::SHA1HashFunction{}.compute(authCookie, authCookieSalt) }; + core::Service::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::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 getUserId() const { return _userId; } + db::UserId getUserId() const { return _userId; } private: - std::optional _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 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::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::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::get()->clearAuthTokens(authTokenDomain, userId); + } + + PasswordAuth::PasswordAuth(auth::IPasswordService& passwordService) : Wt::WTemplateFormView{ Wt::WString::tr("Lms.Auth.template") } { - auto model{ std::make_shared() }; + auto model{ std::make_shared(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()); diff --git a/src/lms/ui/Auth.hpp b/src/lms/ui/Auth.hpp index b2383620..9763d7fe 100644 --- a/src/lms/ui/Auth.hpp +++ b/src/lms/ui/Auth.hpp @@ -19,20 +19,30 @@ #pragma once -#include - #include #include "database/UserId.hpp" +namespace lms::auth +{ + class IPasswordService; +} + namespace lms::ui { - std::optional 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 userLoggedIn; }; diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 71d9249b..b5aa9e9d 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -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 LmsApplication::create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager) + std::unique_ptr LmsApplication::create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend) { - if (auto* authEnvService{ core::Service::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(env); - } - - return std::make_unique(env, db, appManager, checkResult.userId); - } - - return std::make_unique(env, db, appManager); + return std::make_unique(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 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 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::exists()) + db::UserId userId; + switch (_authBackend) + { + case AuthenticationBackend::Env: + { + const auto checkResult{ core::Service::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 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::get()->canSetPasswords()) + assert(_authBackend == AuthenticationBackend::Internal || _authBackend == AuthenticationBackend::PAM); + auth::IPasswordService& passwordService{ *core::Service::get() }; + + if (firstConnection && _authBackend == AuthenticationBackend::Internal) { - root()->addWidget(std::make_unique()); - } - else - { - Auth* auth{ root()->addNew() }; - auth->userLoggedIn.connect(this, [this](db::UserId userId) { - onUserLoggedIn(userId, true /* strongAuth */); - }); + root()->addNew(passwordService); + return; } + + PasswordAuth* auth{ root()->addNew(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::get()->clearAuthTokens("ui", getUserId()); LMS_LOG(UI, INFO, "User '" << getUserLoginName() << " 'logged out"); goHomeAndQuit(); } diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 537add0e..5e59a677 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -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 userId = std::nullopt); + LmsApplication(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager, AuthenticationBackend authBackend); ~LmsApplication(); - static std::unique_ptr create(const Wt::WEnvironment& env, db::Db& db, LmsApplicationManager& appManager); + static std::unique_ptr 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 func); void setTitle(const Wt::WString& title = ""); @@ -88,7 +91,7 @@ namespace lms::ui Wt::Signal<>& preQuit() { return _preQuit; } private: - void init(std::optional 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 { diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 6e3c91ea..d7261e33 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -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; using ScrobblingBackendModel = ValueStringModel; - 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; std::shared_ptr _transcodingModeModeModel; std::shared_ptr> _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::WString::tr("Lms.Settings.template")) }; - auto* authPasswordService{ core::Service::get() }; - if (authPasswordService && !authPasswordService->canSetPasswords()) - authPasswordService = nullptr; - - auto model{ std::make_shared(authPasswordService, !LmsApp->isUserAuthStrong()) }; + auth::IPasswordService* authPasswordService{}; + if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal) + { + authPasswordService = core::Service::get(); + assert(authPasswordService->canSetPasswords()); + } + auto model{ std::make_shared(authPasswordService, !LmsApp->isUserAuthStrong(), *core::Service::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::get()->getBool("api-subsonic", true)); + auto subsonicToken{ std::make_unique() }; + 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::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::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::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()); @@ -555,7 +622,15 @@ namespace lms::ui // Backend settings { auto listenbrainzToken{ std::make_unique() }; + Wt::WLineEdit* listenbrainzTokenPtr{ listenbrainzToken.get() }; + listenbrainzTokenPtr->setEchoMode(Wt::EchoMode::Password); t->setFormWidget(SettingsModel::ListenBrainzTokenField, std::move(listenbrainzToken)); + + auto listenbrainzTokenVisibilityBtn{ std::make_unique(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{ [=] { diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index 0a4ca1eb..3cec9844 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -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(valueText(AdminLoginField).toUTF8()) }; user.modify()->setType(db::UserType::ADMIN); - core::Service::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(); + auto model = std::make_shared(passwordService); // AdminLogin { diff --git a/src/lms/ui/admin/InitWizardView.hpp b/src/lms/ui/admin/InitWizardView.hpp index dd416ea0..318704fb 100644 --- a/src/lms/ui/admin/InitWizardView.hpp +++ b/src/lms/ui/admin/InitWizardView.hpp @@ -21,13 +21,17 @@ #include +namespace lms::auth +{ + class IPasswordService; +} + namespace lms::ui { - class InitWizardView : public Wt::WTemplateFormView { public: - InitWizardView(); + InitWizardView(auth::IPasswordService& passwordService); }; } // namespace lms::ui diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index cfb15b81..123ef5ea 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -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, auth::IPasswordService* authPasswordService) + UserModel(std::optional 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::get()->createAuthToken("subsonic", user->getId(), core::UUID::generate().getAsString()); } } @@ -177,6 +185,7 @@ namespace lms::ui std::optional _userId; auth::IPasswordService* _authPasswordService{}; + auth::IAuthTokenService& _authTokenService; }; UserView::UserView() @@ -199,12 +208,14 @@ namespace lms::ui Wt::WTemplateFormView* t{ addNew(Wt::WString::tr("Lms.Admin.User.template")) }; - auto* authPasswordService{ core::Service::get() }; - if (authPasswordService && !authPasswordService->canSetPasswords()) - authPasswordService = nullptr; - - auto model{ std::make_shared(userId, authPasswordService) }; + auth::IPasswordService* authPasswordService{}; + if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal) + { + authPasswordService = core::Service::get(); + assert(authPasswordService->canSetPasswords()); + } + auto model{ std::make_shared(userId, authPasswordService, *core::Service::get()) }; if (userId) { auto transaction{ LmsApp->getDbSession().createReadTransaction() }; diff --git a/src/lms/ui/admin/UsersView.cpp b/src/lms/ui/admin/UsersView.cpp index 7ad07c99..65d08173 100644 --- a/src/lms/ui/admin/UsersView.cpp +++ b/src/lms/ui/admin/UsersView.cpp @@ -43,7 +43,7 @@ namespace lms::ui _container = bindNew("users"); - if (core::Service::get() && core::Service::get()->canSetPasswords()) + if (LmsApp->getAuthBackend() == AuthenticationBackend::Internal) { setCondition("if-can-create-user", true); diff --git a/src/lms/ui/common/PasswordValidator.cpp b/src/lms/ui/common/PasswordValidator.cpp index f6cb31d6..0c22c793 100644 --- a/src/lms/ui/common/PasswordValidator.cpp +++ b/src/lms/ui/common/PasswordValidator.cpp @@ -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::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 createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc) + std::unique_ptr createPasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc) { - return std::make_unique(std::move(passwordValidationContextGetFunc)); + return std::make_unique(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::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 createPasswordCheckValidator() + std::unique_ptr createPasswordCheckValidator(auth::IPasswordService& passwordService) { - return std::make_unique(); + return std::make_unique(passwordService); } } // namespace lms::ui \ No newline at end of file diff --git a/src/lms/ui/common/PasswordValidator.hpp b/src/lms/ui/common/PasswordValidator.hpp index adeeebbe..8135716c 100644 --- a/src/lms/ui/common/PasswordValidator.hpp +++ b/src/lms/ui/common/PasswordValidator.hpp @@ -25,11 +25,16 @@ #include "services/auth/Types.hpp" +namespace lms::auth +{ + class IPasswordService; +} + namespace lms::ui { using PasswordValidationContextGetFunc = std::function; - std::unique_ptr createPasswordStrengthValidator(PasswordValidationContextGetFunc passwordValidationContextGetFunc); + std::unique_ptr createPasswordStrengthValidator(const auth::IPasswordService& passwordService, PasswordValidationContextGetFunc passwordValidationContextGetFunc); // Check current user password - std::unique_ptr createPasswordCheckValidator(); + std::unique_ptr createPasswordCheckValidator(auth::IPasswordService& passwordService); } // namespace lms::ui