OpenSubsonic API: added apiKey support, ref #544

This commit is contained in:
emeric
2024-11-24 15:23:14 +01:00
parent 0a320a8b87
commit 360623c569
54 changed files with 870 additions and 623 deletions
+49 -13
View File
@@ -30,29 +30,65 @@
namespace lms::db
{
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
: _value{ value }
AuthToken::AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user)
: _domain{ domain }
, _value{ value }
, _expiry{ expiry }
, _maxUseCount{ maxUseCount }
, _user{ getDboPtr(user) }
{
}
AuthToken::pointer AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
AuthToken::pointer AuthToken::create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user)
{
return session.getDboSession()->add(std::unique_ptr<AuthToken>{ new AuthToken{ value, expiry, user } });
return session.getDboSession()->add(std::unique_ptr<AuthToken>{ new AuthToken{ domain, value, expiry, maxUseCount, user } });
}
void AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
{
session.checkWriteTransaction();
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ?", now);
}
AuthToken::pointer AuthToken::find(Session& session, std::string_view value)
std::size_t AuthToken::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<AuthToken>().where("value = ?").bind(value));
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM auth_token"));
}
AuthToken::pointer AuthToken::find(Session& session, AuthTokenId id)
{
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<AuthToken>>("SELECT a_t from auth_token a_t").where("a_t.id = ?").bind(id));
}
AuthToken::pointer AuthToken::find(Session& session, std::string_view domain, std::string_view value)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<AuthToken>() };
query.where("domain = ?").bind(domain);
query.where("value = ?").bind(value);
return utils::fetchQuerySingleResult(query);
}
void AuthToken::find(Session& session, std::string_view domain, UserId userId, std::function<void(const AuthToken::pointer&)> visitor)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<AuthToken>() };
query.where("domain = ?").bind(domain);
query.where("user_id = ?").bind(userId);
utils::forEachQueryResult(query, visitor);
}
void AuthToken::removeExpiredTokens(Session& session, std::string_view domain, const Wt::WDateTime& now)
{
session.checkWriteTransaction();
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE expiry < ? AND domain = ?", now, domain);
}
void AuthToken::clearUserTokens(Session& session, std::string_view domain, UserId user)
{
session.checkWriteTransaction();
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token WHERE user_id = ? AND domain = ?", user, domain);
}
} // namespace lms::db
+17 -1
View File
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 74 };
static constexpr Version LMS_DATABASE_VERSION{ 75 };
}
VersionInfo::VersionInfo()
@@ -947,6 +947,21 @@ SELECT
utils::executeCommand(*session.getDboSession(), "UPDATE media_library SET path = rtrim(path, '/') WHERE path LIKE '%/'");
}
void migrateFromV74(Session& session)
{
// New auth token authentication for Subsonic API
// Previous tokens are not usable any more, no problem since they are just used for the ui's "remember me" feature
utils::executeCommand(*session.getDboSession(), "DELETE FROM auth_token");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD domain TEXT NOT NULL");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD use_count INTEGER NOT NULL");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD last_used TEXT");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE auth_token ADD max_use_count INTEGER");
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_user_idx");
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_expiry_idx");
utils::executeCommand(*session.getDboSession(), "DROP INDEX IF EXISTS auth_token_value_idx");
}
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -997,6 +1012,7 @@ SELECT
{ 71, migrateFromV71 },
{ 72, migrateFromV72 },
{ 73, migrateFromV73 },
{ 74, migrateFromV74 },
};
bool migrationPerformed{};
+3 -3
View File
@@ -188,9 +188,9 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
-6
View File
@@ -97,10 +97,4 @@ namespace lms::db
assert(isAudioBitrateAllowed(bitrate));
_subsonicDefaultTranscodingOutputBitrate = bitrate;
}
void User::clearAuthTokens()
{
_authTokens.clear();
}
} // namespace lms::db
@@ -19,6 +19,7 @@
#pragma once
#include <optional>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
@@ -26,41 +27,62 @@
#include "database/AuthTokenId.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/UserId.hpp"
namespace lms::db
{
class Session;
class User;
class AuthToken final : public Object<AuthToken, AuthTokenId>
{
public:
AuthToken() = default;
// Utility
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
static pointer find(Session& session, std::string_view value);
static std::size_t getCount(Session& session);
static pointer find(Session& session, AuthTokenId tokenId);
static pointer find(Session& session, std::string_view domain, std::string_view value);
static void find(Session& session, std::string_view domain, UserId userId, std::function<void(const AuthToken::pointer&)> visitor);
static void removeExpiredTokens(Session& session, std::string_view domain, const Wt::WDateTime& now);
static void clearUserTokens(Session& session, std::string_view domain, UserId user);
// Accessors
const Wt::WDateTime& getExpiry() const { return _expiry; }
ObjectPtr<User> getUser() const { return _user; }
const std::string& getValue() const { return _value; }
std::size_t getUseCount() const { return _useCount; }
Wt::WDateTime getLastUsed() const { return _lastUsed; }
std::optional<std::size_t> getMaxUseCount() const { return _maxUseCount; }
// Setters
std::size_t incUseCount() { return ++_useCount; }
void setLastUsed(const Wt::WDateTime& lastUsed) { _lastUsed = lastUsed; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _domain, "domain");
Wt::Dbo::field(a, _value, "value");
Wt::Dbo::field(a, _expiry, "expiry");
Wt::Dbo::field(a, _useCount, "use_count");
Wt::Dbo::field(a, _lastUsed, "last_used");
Wt::Dbo::field(a, _maxUseCount, "max_use_count");
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
static pointer create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
AuthToken(std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user);
static pointer create(Session& session, std::string_view domain, std::string_view value, const Wt::WDateTime& expiry, std::optional<long> maxUseCount, ObjectPtr<User> user);
std::string _domain;
std::string _value;
Wt::WDateTime _expiry;
long _useCount{};
Wt::WDateTime _lastUsed;
std::optional<long> _maxUseCount;
Wt::Dbo::ptr<User> _user;
};
} // namespace lms::db
} // namespace lms::db
@@ -108,7 +108,6 @@ namespace lms::db
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
+49
View File
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Common.hpp"
#include "database/AuthToken.hpp"
namespace lms::db::tests
{
using ScopedAuthToken = ScopedEntity<db::AuthToken>;
TEST_F(DatabaseFixture, AuthTokens)
{
ScopedUser user{ session, "MyUser" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(AuthToken::getCount(session), 0);
}
ScopedAuthToken token{ session, "myDomain", "foo", Wt::WDateTime{}, std::nullopt, user.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(AuthToken::getCount(session), 1);
}
{
auto transaction{ session.createWriteTransaction() };
AuthToken::clearUserTokens(session, "nonExistingDomain", user.getId());
}
}
} // namespace lms::db::tests
+1
View File
@@ -1,5 +1,6 @@
add_executable(test-database
AuthToken.cpp
Artist.cpp
Cluster.cpp
Common.cpp
+2
View File
@@ -20,6 +20,7 @@
#include "Common.hpp"
#include "core/String.hpp"
#include "database/AuthToken.hpp"
#include "database/Db.hpp"
#include "database/Directory.hpp"
#include "database/Image.hpp"
@@ -337,6 +338,7 @@ VALUES
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Artist::find(session, ArtistId{}));
EXPECT_FALSE(AuthToken::find(session, AuthTokenId{}));
EXPECT_FALSE(Cluster::find(session, ClusterId{}));
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{}));