OpenSubsonic API: added apiKey support, ref #544
This commit is contained in:
@@ -27,10 +27,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
struct ClientInfo
|
||||
{
|
||||
std::string ipAddress;
|
||||
std::string name;
|
||||
std::string user;
|
||||
std::string password;
|
||||
ProtocolVersion version;
|
||||
};
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -41,7 +41,8 @@ namespace lms::api::subsonic
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters;
|
||||
db::Session& dbSession;
|
||||
const db::ObjectPtr<db::User> user;
|
||||
db::ObjectPtr<db::User> user;
|
||||
std::string clientIpAddr;
|
||||
ClientInfo clientInfo;
|
||||
ProtocolVersion serverProtocolVersion;
|
||||
ResponseFormat responseFormat;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/IEnvService.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "ParameterParsing.hpp"
|
||||
@@ -41,7 +41,6 @@
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "entrypoints/AlbumSongLists.hpp"
|
||||
#include "entrypoints/Bookmarks.hpp"
|
||||
#include "entrypoints/Browsing.hpp"
|
||||
@@ -132,9 +131,10 @@ namespace lms::api::subsonic
|
||||
return res;
|
||||
}
|
||||
|
||||
void checkUserTypeIsAllowed(RequestContext& context, core::EnumSet<db::UserType> allowedUserTypes)
|
||||
void checkUserTypeIsAllowed(const db::User::pointer& user, core::EnumSet<db::UserType> allowedUserTypes)
|
||||
{
|
||||
if (!allowedUserTypes.contains(context.user->getType()))
|
||||
assert(user);
|
||||
if (!allowedUserTypes.contains(user->getType()))
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
@@ -143,20 +143,24 @@ namespace lms::api::subsonic
|
||||
throw NotImplementedGenericError{};
|
||||
}
|
||||
|
||||
enum class AuthenticationMode
|
||||
{
|
||||
Authenticated,
|
||||
Unauthenticated,
|
||||
};
|
||||
using RequestHandlerFunc = std::function<Response(RequestContext& context)>;
|
||||
using CheckImplementedFunc = std::function<void()>;
|
||||
struct RequestEntryPointInfo
|
||||
{
|
||||
RequestHandlerFunc func;
|
||||
AuthenticationMode authMode{ AuthenticationMode::Authenticated };
|
||||
core::EnumSet<db::UserType> allowedUserTypes{ db::UserType::DEMO, db::UserType::REGULAR, db::UserType::ADMIN };
|
||||
CheckImplementedFunc checkFunc{};
|
||||
};
|
||||
|
||||
const std::unordered_map<core::LiteralString, RequestEntryPointInfo, core::LiteralStringHash, core::LiteralStringEqual> requestEntryPoints{
|
||||
// System
|
||||
{ "/ping", { handlePingRequest } },
|
||||
{ "/getLicense", { handleGetLicenseRequest } },
|
||||
{ "/getOpenSubsonicExtensions", { handleGetOpenSubsonicExtensions } },
|
||||
{ "/getOpenSubsonicExtensions", { handleGetOpenSubsonicExtensions, AuthenticationMode::Unauthenticated } },
|
||||
|
||||
// Browsing
|
||||
{ "/getMusicFolders", { handleGetMusicFoldersRequest } },
|
||||
@@ -240,11 +244,11 @@ namespace lms::api::subsonic
|
||||
|
||||
// User management
|
||||
{ "/getUser", { handleGetUserRequest } },
|
||||
{ "/getUsers", { handleGetUsersRequest, { db::UserType::ADMIN } } },
|
||||
{ "/createUser", { handleCreateUserRequest, { db::UserType::ADMIN }, &utils::checkSetPasswordImplemented } },
|
||||
{ "/updateUser", { handleUpdateUserRequest, { db::UserType::ADMIN } } },
|
||||
{ "/deleteUser", { handleDeleteUserRequest, { db::UserType::ADMIN } } },
|
||||
{ "/changePassword", { handleChangePassword, { db::UserType::REGULAR, db::UserType::ADMIN }, &utils::checkSetPasswordImplemented } },
|
||||
{ "/getUsers", { handleGetUsersRequest, AuthenticationMode::Authenticated, { db::UserType::ADMIN } } },
|
||||
{ "/createUser", { handleNotImplemented } },
|
||||
{ "/updateUser", { handleNotImplemented } },
|
||||
{ "/deleteUser", { handleNotImplemented } },
|
||||
{ "/changePassword", { handleNotImplemented } },
|
||||
|
||||
// Bookmarks
|
||||
{ "/getBookmarks", { handleGetBookmarks } },
|
||||
@@ -255,7 +259,7 @@ namespace lms::api::subsonic
|
||||
|
||||
// Media library scanning
|
||||
{ "/getScanStatus", { Scan::handleGetScanStatus } },
|
||||
{ "/startScan", { Scan::handleStartScan, { db::UserType::ADMIN } } },
|
||||
{ "/startScan", { Scan::handleStartScan, AuthenticationMode::Authenticated, { db::UserType::ADMIN } } },
|
||||
};
|
||||
|
||||
using MediaRetrievalHandlerFunc = std::function<void(RequestContext&, const Wt::Http::Request&, Wt::Http::Response&)>;
|
||||
@@ -278,6 +282,16 @@ namespace lms::api::subsonic
|
||||
TLSMonotonicMemoryResourceCleaner(const TLSMonotonicMemoryResourceCleaner&) = delete;
|
||||
TLSMonotonicMemoryResourceCleaner& operator=(const TLSMonotonicMemoryResourceCleaner&) = delete;
|
||||
};
|
||||
|
||||
db::User::pointer getUserFromUserId(db::Session& session, db::UserId userId)
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
if (db::User::pointer user{ db::User::find(session, userId) })
|
||||
return user;
|
||||
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SubsonicResource::SubsonicResource(db::Db& db)
|
||||
@@ -317,10 +331,11 @@ namespace lms::api::subsonic
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itEntryPoint->first);
|
||||
|
||||
if (itEntryPoint->second.checkFunc)
|
||||
itEntryPoint->second.checkFunc();
|
||||
|
||||
checkUserTypeIsAllowed(requestContext, itEntryPoint->second.allowedUserTypes);
|
||||
if (itEntryPoint->second.authMode == AuthenticationMode::Authenticated)
|
||||
{
|
||||
requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
|
||||
checkUserTypeIsAllowed(requestContext.user, itEntryPoint->second.allowedUserTypes);
|
||||
}
|
||||
|
||||
const Response resp{ [&] {
|
||||
LMS_SCOPED_TRACE_DETAILED("Subsonic", "HandleRequest");
|
||||
@@ -343,11 +358,19 @@ namespace lms::api::subsonic
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Subsonic", itStreamHandler->first);
|
||||
|
||||
// Media retrieval endpoints are always authenticated
|
||||
// Optim: no need to reauth user for each continuation
|
||||
if (!request.continuation())
|
||||
requestContext.user = getUserFromUserId(_db.getTLSSession(), authenticateUser(request));
|
||||
|
||||
itStreamHandler->second(requestContext, request, response);
|
||||
LMS_LOG(API_SUBSONIC, DEBUG, "Request " << requestId << " '" << requestPath << "' handled!");
|
||||
return;
|
||||
}
|
||||
|
||||
// do not disclose unhandled commands for unauthenticated users
|
||||
authenticateUser(request);
|
||||
|
||||
LMS_LOG(API_SUBSONIC, ERROR, "Unhandled command '" << requestPath << "'");
|
||||
throw UnknownEntryPointGenericError{};
|
||||
}
|
||||
@@ -391,16 +414,9 @@ namespace lms::api::subsonic
|
||||
const auto& parameters{ request.getParameterMap() };
|
||||
ClientInfo res;
|
||||
|
||||
if (hasParameter(parameters, "t"))
|
||||
throw TokenAuthenticationNotSupportedForLDAPUsersError{};
|
||||
|
||||
res.ipAddress = request.clientAddress();
|
||||
|
||||
// Mandatory parameters
|
||||
res.name = getMandatoryParameterAs<std::string>(parameters, "c");
|
||||
res.version = getMandatoryParameterAs<ProtocolVersion>(parameters, "v");
|
||||
res.user = getMandatoryParameterAs<std::string>(parameters, "u");
|
||||
res.password = decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(parameters, "p"));
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -409,25 +425,15 @@ namespace lms::api::subsonic
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
|
||||
const ClientInfo clientInfo{ getClientInfo(request) };
|
||||
const db::UserId userId{ authenticateUser(request, clientInfo) };
|
||||
bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) };
|
||||
bool enableDefaultCover{ _defaultReleaseCoverClients.contains(clientInfo.name) };
|
||||
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
|
||||
|
||||
db::User::pointer user;
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
user = db::User::find(session, userId);
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return RequestContext{
|
||||
.parameters = parameters,
|
||||
.dbSession = _db.getTLSSession(),
|
||||
.user = user,
|
||||
.user = db::User::pointer{},
|
||||
.clientIpAddr = request.clientAddress(),
|
||||
.clientInfo = clientInfo,
|
||||
.serverProtocolVersion = getServerProtocolVersion(clientInfo.name),
|
||||
.responseFormat = format,
|
||||
@@ -436,46 +442,49 @@ namespace lms::api::subsonic
|
||||
};
|
||||
}
|
||||
|
||||
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
|
||||
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request)
|
||||
{
|
||||
// if the request if a continuation, the user is already authenticated
|
||||
if (request.continuation())
|
||||
const auto& parameters{ request.getParameterMap() };
|
||||
|
||||
if (hasParameter(parameters, "t"))
|
||||
throw TokenAuthenticationNotSupportedForLDAPUsersError{};
|
||||
|
||||
const auto user{ getParameterAs<std::string>(parameters, "u") };
|
||||
const auto password{ getParameterAs<std::string>(parameters, "p") };
|
||||
const auto apiKey{ getParameterAs<std::string>(parameters, "apiKey") };
|
||||
|
||||
if (user && !password)
|
||||
throw RequiredParameterMissingError{ "p" };
|
||||
if (!user && password)
|
||||
throw RequiredParameterMissingError{ "u" };
|
||||
if (apiKey && password)
|
||||
throw MultipleConflictingAuthenticationMechanismsProvidedError{};
|
||||
if (!apiKey && !password)
|
||||
throw RequiredParameterMissingError{ "apiKey" };
|
||||
|
||||
const auto clientAddress{ boost::asio::ip::address::from_string(request.clientAddress()) };
|
||||
const std::string authToken{ apiKey ? *apiKey : decodePasswordIfNeeded(*password) };
|
||||
|
||||
const auto authResult{ core::Service<auth::IAuthTokenService>::get()->processAuthToken("subsonic", clientAddress, authToken) };
|
||||
switch (authResult.state)
|
||||
{
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto user{ db::User::find(session, clientInfo.user) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
return user->getId();
|
||||
}
|
||||
|
||||
if (auto* authEnvService{ core::Service<auth::IEnvService>::get() })
|
||||
{
|
||||
const auto checkResult{ authEnvService->processRequest(request) };
|
||||
if (checkResult.state != auth::IEnvService::CheckResult::State::Granted)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
return *checkResult.userId;
|
||||
}
|
||||
else if (auto* authPasswordService{ core::Service<auth::IPasswordService>::get() })
|
||||
{
|
||||
const auto checkResult{ authPasswordService->checkUserPassword(boost::asio::ip::address::from_string(request.clientAddress()), clientInfo.user, clientInfo.password) };
|
||||
|
||||
switch (checkResult.state)
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Granted:
|
||||
if (user)
|
||||
{
|
||||
case auth::IPasswordService::CheckResult::State::Granted:
|
||||
return *checkResult.userId;
|
||||
break;
|
||||
case auth::IPasswordService::CheckResult::State::Denied:
|
||||
throw WrongUsernameOrPasswordError{};
|
||||
case auth::IPasswordService::CheckResult::State::Throttled:
|
||||
throw LoginThrottledGenericError{};
|
||||
const auto authenticatedUser{ getUserFromUserId(_db.getTLSSession(), authResult.authTokenInfo->userId) };
|
||||
if (!authenticatedUser || authenticatedUser->getLoginName() != *user)
|
||||
throw WrongUsernameOrPasswordError{};
|
||||
}
|
||||
return authResult.authTokenInfo->userId;
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Denied:
|
||||
if (apiKey)
|
||||
throw InvalidAPIkeyError{};
|
||||
else
|
||||
throw WrongUsernameOrPasswordError{};
|
||||
case auth::IAuthTokenService::AuthTokenProcessResult::State::Throttled:
|
||||
throw LoginThrottledGenericError{};
|
||||
}
|
||||
|
||||
throw InternalErrorGenericError{ "No service available to authenticate user" };
|
||||
throw InternalErrorGenericError{ "Cannot authenticate user" };
|
||||
}
|
||||
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace lms::api::subsonic
|
||||
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
||||
ClientInfo getClientInfo(const Wt::Http::Request& request);
|
||||
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
||||
db::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
||||
db::UserId authenticateUser(const Wt::Http::Request& request);
|
||||
|
||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||
const std::unordered_set<std::string> _openSubsonicDisabledClients;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
namespace lms::api::subsonic
|
||||
{
|
||||
// Max count expected from all API methods that expose a count
|
||||
static inline constexpr std::size_t defaultMaxCountSize{ 1000 };
|
||||
static inline constexpr std::size_t defaultMaxCountSize{ 1'000 };
|
||||
|
||||
enum class ResponseFormat
|
||||
{
|
||||
@@ -54,6 +54,9 @@ namespace lms::api::subsonic
|
||||
ServerMustUpgrade = 30,
|
||||
WrongUsernameOrPassword = 40,
|
||||
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
||||
ProvidedAuthenticationMechanismNotSupported = 42,
|
||||
MultipleConflictingAuthenticationMechanismsProvided = 43,
|
||||
InvalidAPIkey = 44,
|
||||
UserNotAuthorized = 50,
|
||||
RequestedDataNotFound = 70,
|
||||
};
|
||||
@@ -130,6 +133,45 @@ namespace lms::api::subsonic
|
||||
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
||||
};
|
||||
|
||||
class ProvidedAuthenticationMechanismNotSupportedError : public Error
|
||||
{
|
||||
public:
|
||||
ProvidedAuthenticationMechanismNotSupportedError()
|
||||
: Error{ Code::ProvidedAuthenticationMechanismNotSupported } {}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override
|
||||
{
|
||||
return "Provided authentication mechanism not supported.";
|
||||
}
|
||||
};
|
||||
|
||||
class MultipleConflictingAuthenticationMechanismsProvidedError : public Error
|
||||
{
|
||||
public:
|
||||
MultipleConflictingAuthenticationMechanismsProvidedError()
|
||||
: Error{ Code::MultipleConflictingAuthenticationMechanismsProvided } {}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override
|
||||
{
|
||||
return "Multiple conflicting authentication mechanisms provided.";
|
||||
}
|
||||
};
|
||||
|
||||
class InvalidAPIkeyError : public Error
|
||||
{
|
||||
public:
|
||||
InvalidAPIkeyError()
|
||||
: Error{ Code::InvalidAPIkey } {}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override
|
||||
{
|
||||
return "Invalid API key.";
|
||||
}
|
||||
};
|
||||
|
||||
class UserNotAuthorizedError : public Error
|
||||
{
|
||||
public:
|
||||
@@ -153,7 +195,7 @@ namespace lms::api::subsonic
|
||||
class InternalErrorGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
InternalErrorGenericError(const std::string& message)
|
||||
InternalErrorGenericError(std::string_view message)
|
||||
: _message{ message } {}
|
||||
|
||||
private:
|
||||
@@ -176,26 +218,6 @@ namespace lms::api::subsonic
|
||||
std::string getMessage() const override { return "Unknown API method"; }
|
||||
};
|
||||
|
||||
class PasswordTooWeakGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password too weak"; }
|
||||
};
|
||||
|
||||
class PasswordMustMatchLoginNameGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password must match login name"; }
|
||||
};
|
||||
|
||||
class DemoUserCannotChangePasswordGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
||||
};
|
||||
|
||||
class UserAlreadyExistsGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "User already exists"; }
|
||||
};
|
||||
|
||||
class BadParameterGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include "core/Service.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace lms::api::subsonic::utils
|
||||
{
|
||||
void checkSetPasswordImplemented()
|
||||
{
|
||||
auth::IPasswordService* passwordService{ core::Service<auth::IPasswordService>::get() };
|
||||
if (!passwordService || !passwordService->canSetPasswords())
|
||||
throw NotImplementedGenericError{};
|
||||
}
|
||||
|
||||
std::string makeNameFilesystemCompatible(std::string_view name)
|
||||
{
|
||||
return core::stringUtils::replaceInString(name, "/", "_");
|
||||
}
|
||||
|
||||
} // namespace lms::api::subsonic::utils
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace lms::api::subsonic::utils
|
||||
{
|
||||
void checkSetPasswordImplemented();
|
||||
std::string makeNameFilesystemCompatible(std::string_view name);
|
||||
} // namespace lms::api::subsonic::utils
|
||||
@@ -36,7 +36,6 @@
|
||||
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "responses/Album.hpp"
|
||||
#include "responses/AlbumInfo.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace lms::api::subsonic
|
||||
{
|
||||
std::string clientAddress;
|
||||
std::string clientName;
|
||||
std::string userName;
|
||||
UserId user;
|
||||
MediaLibraryId library;
|
||||
std::size_t offset{};
|
||||
auto operator<=>(const ScanInfo&) const = default;
|
||||
@@ -177,9 +177,9 @@ namespace lms::api::subsonic
|
||||
else
|
||||
{
|
||||
ScanTracker<ArtistId>::ScanInfo scanInfo{
|
||||
.clientAddress = context.clientInfo.ipAddress,
|
||||
.clientAddress = context.clientIpAddr,
|
||||
.clientName = context.clientInfo.name,
|
||||
.userName = context.clientInfo.user,
|
||||
.user = context.user->getId(),
|
||||
.library = mediaLibrary,
|
||||
.offset = artistOffset
|
||||
};
|
||||
@@ -241,9 +241,9 @@ namespace lms::api::subsonic
|
||||
else
|
||||
{
|
||||
ScanTracker<ReleaseId>::ScanInfo scanInfo{
|
||||
.clientAddress = context.clientInfo.ipAddress,
|
||||
.clientAddress = context.clientIpAddr,
|
||||
.clientName = context.clientInfo.name,
|
||||
.userName = context.clientInfo.user,
|
||||
.user = context.user->getId(),
|
||||
.library = mediaLibrary,
|
||||
.offset = albumOffset
|
||||
};
|
||||
@@ -305,9 +305,9 @@ namespace lms::api::subsonic
|
||||
else
|
||||
{
|
||||
ScanTracker<TrackId>::ScanInfo scanInfo{
|
||||
.clientAddress = context.clientInfo.ipAddress,
|
||||
.clientAddress = context.clientIpAddr,
|
||||
.clientName = context.clientInfo.name,
|
||||
.userName = context.clientInfo.user,
|
||||
.user = context.user->getId(),
|
||||
.library = mediaLibrary,
|
||||
.offset = songOffset
|
||||
};
|
||||
|
||||
@@ -41,6 +41,12 @@ namespace lms::api::subsonic
|
||||
songLyricsNode.addArrayValue("versions", 1);
|
||||
}
|
||||
|
||||
{
|
||||
Response::Node& apiKeyAuthentication{ response.createArrayNode("openSubsonicExtensions") };
|
||||
apiKeyAuthentication.setAttribute("name", "apiKeyAuthentication");
|
||||
apiKeyAuthentication.addArrayValue("versions", 1);
|
||||
}
|
||||
|
||||
return response;
|
||||
};
|
||||
} // namespace lms::api::subsonic
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "responses/User.hpp"
|
||||
|
||||
namespace lms::api::subsonic
|
||||
@@ -52,150 +51,4 @@ namespace lms::api::subsonic
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleCreateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||
// Just ignore all the other fields as we don't handle them
|
||||
|
||||
db::UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (user)
|
||||
throw UserAlreadyExistsGenericError{};
|
||||
|
||||
user = context.dbSession.create<User>(username);
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
auto removeCreatedUser{ [&] {
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
User::pointer user{ User::find(context.dbSession, userId) };
|
||||
if (user)
|
||||
user.remove();
|
||||
} };
|
||||
|
||||
try
|
||||
{
|
||||
core::Service<auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const auth::PasswordTooWeakException&)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const auth::Exception& exception)
|
||||
{
|
||||
removeCreatedUser();
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleDeleteUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
|
||||
auto transaction{ context.dbSession.createWriteTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
// cannot delete ourself
|
||||
if (user->getId() == context.user->getId())
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
user.remove();
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleUpdateUserRequest(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::optional<std::string> password{ getParameterAs<std::string>(context.parameters, "password") };
|
||||
|
||||
UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
if (password)
|
||||
{
|
||||
utils::checkSetPasswordImplemented();
|
||||
|
||||
try
|
||||
{
|
||||
core::Service<auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password));
|
||||
}
|
||||
catch (const auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const auth::Exception&)
|
||||
{
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleChangePassword(RequestContext& context)
|
||||
{
|
||||
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||
|
||||
try
|
||||
{
|
||||
db::UserId userId;
|
||||
{
|
||||
auto transaction{ context.dbSession.createReadTransaction() };
|
||||
|
||||
checkUserIsMySelfOrAdmin(context, username);
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, username) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
userId = user->getId();
|
||||
}
|
||||
|
||||
core::Service<auth::IPasswordService>::get()->setPassword(userId, password);
|
||||
}
|
||||
catch (const auth::PasswordMustMatchLoginNameException&)
|
||||
{
|
||||
throw PasswordMustMatchLoginNameGenericError{};
|
||||
}
|
||||
catch (const auth::PasswordTooWeakException&)
|
||||
{
|
||||
throw PasswordTooWeakGenericError{};
|
||||
}
|
||||
catch (const auth::Exception& authException)
|
||||
{
|
||||
throw UserNotAuthorizedError{};
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
} // namespace lms::api::subsonic
|
||||
@@ -38,7 +38,6 @@
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
#include "responses/Contributor.hpp"
|
||||
#include "responses/ItemGenre.hpp"
|
||||
|
||||
Reference in New Issue
Block a user