mirror of
https://github.com/badaix/snapcast.git
synced 2025-06-19 19:21:43 +02:00
Read users and roles, remove JWT
This commit is contained in:
parent
9247588764
commit
42f4c39e6c
13 changed files with 144 additions and 68 deletions
|
@ -103,7 +103,7 @@ struct ClientSettings
|
|||
/// Log settings
|
||||
struct Logging
|
||||
{
|
||||
/// The log sink (null,system,stdout,stderr,file:<filename>)
|
||||
/// The log sink (null,system,stdout,stderr,file)
|
||||
std::string sink;
|
||||
/// Log filter
|
||||
std::string filter{"*:info"};
|
||||
|
|
|
@ -6,7 +6,7 @@ set(SERVER_SOURCES
|
|||
control_session_tcp.cpp
|
||||
control_session_http.cpp
|
||||
control_session_ws.cpp
|
||||
jwt.cpp
|
||||
# jwt.cpp
|
||||
snapserver.cpp
|
||||
server.cpp
|
||||
stream_server.cpp
|
||||
|
|
|
@ -23,14 +23,12 @@
|
|||
#include "common/aixlog.hpp"
|
||||
#include "common/base64.h"
|
||||
#include "common/utils/string_utils.hpp"
|
||||
#include "jwt.hpp"
|
||||
|
||||
// 3rd party headers
|
||||
|
||||
// standard headers
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
|
@ -100,14 +98,15 @@ std::error_code make_error_code(AuthErrc errc)
|
|||
}
|
||||
|
||||
|
||||
AuthInfo::AuthInfo(const ServerSettings& settings) : has_auth_info_(false), settings_(settings)
|
||||
AuthInfo::AuthInfo(ServerSettings::Authorization settings) : has_auth_info_(false), settings_(std::move(settings))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
ErrorCode AuthInfo::validateUser(const std::string& username, const std::optional<std::string>& password) const
|
||||
{
|
||||
auto iter = std::find_if(settings_.users.begin(), settings_.users.end(), [&](const ServerSettings::User& user) { return user.name == username; });
|
||||
auto iter =
|
||||
std::find_if(settings_.users.begin(), settings_.users.end(), [&](const ServerSettings::Authorization::User& user) { return user.name == username; });
|
||||
if (iter == settings_.users.end())
|
||||
return ErrorCode{AuthErrc::unknown_user};
|
||||
if (password.has_value() && (iter->password != password.value()))
|
||||
|
@ -120,12 +119,12 @@ ErrorCode AuthInfo::authenticate(const std::string& scheme, const std::string& p
|
|||
{
|
||||
std::string scheme_normed = utils::string::trim_copy(utils::string::tolower_copy(scheme));
|
||||
std::string param_normed = utils::string::trim_copy(param);
|
||||
if (scheme_normed == "bearer")
|
||||
return authenticateBearer(param_normed);
|
||||
else if (scheme_normed == "basic")
|
||||
// if (scheme_normed == "bearer")
|
||||
// return authenticateBearer(param_normed);
|
||||
if (scheme_normed == "basic")
|
||||
return authenticateBasic(param_normed);
|
||||
|
||||
return {AuthErrc::auth_scheme_not_supported, "Scheme must be 'Basic' or 'Bearer'"};
|
||||
return {AuthErrc::auth_scheme_not_supported, "Scheme must be 'Basic'"}; // or 'Bearer'"};
|
||||
}
|
||||
|
||||
|
||||
|
@ -151,7 +150,7 @@ ErrorCode AuthInfo::authenticateBasic(const std::string& credentials)
|
|||
return ec;
|
||||
}
|
||||
|
||||
|
||||
#if 0
|
||||
ErrorCode AuthInfo::authenticateBearer(const std::string& token)
|
||||
{
|
||||
has_auth_info_ = false;
|
||||
|
@ -198,6 +197,7 @@ ErrorOr<std::string> AuthInfo::getToken(const std::string& username, const std::
|
|||
return ErrorCode{AuthErrc::failed_to_create_token};
|
||||
return token.value();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
bool AuthInfo::isExpired() const
|
||||
|
@ -236,19 +236,25 @@ const std::string& AuthInfo::username() const
|
|||
|
||||
bool AuthInfo::hasPermission(const std::string& resource) const
|
||||
{
|
||||
if (!settings_.enabled)
|
||||
return true;
|
||||
|
||||
if (!hasAuthInfo())
|
||||
return false;
|
||||
|
||||
auto iter = std::find_if(settings_.users.begin(), settings_.users.end(), [&](const ServerSettings::User& user) { return user.name == username_; });
|
||||
if (iter == settings_.users.end())
|
||||
const auto& user_iter = std::find_if(settings_.users.begin(), settings_.users.end(), [&](const auto& user) { return user.name == username_; });
|
||||
if (user_iter == settings_.users.end())
|
||||
return false;
|
||||
|
||||
auto perm_iter = std::find_if(iter->permissions.begin(), iter->permissions.end(),
|
||||
const auto& role = user_iter->role;
|
||||
auto perm_iter = std::find_if(role->permissions.begin(), role->permissions.end(),
|
||||
[&](const std::string& permission) { return utils::string::wildcardMatch(permission, resource); });
|
||||
if (perm_iter != iter->permissions.end())
|
||||
|
||||
if (perm_iter != role->permissions.end())
|
||||
{
|
||||
LOG(DEBUG, LOG_TAG) << "Found permission for ressource '" << resource << "': '" << *perm_iter << "'\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/***
|
||||
This file is part of snapcast
|
||||
Copyright (C) 2014-2024 Johannes Pohl
|
||||
Copyright (C) 2014-2025 Johannes Pohl
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -66,7 +66,7 @@ class AuthInfo
|
|||
{
|
||||
public:
|
||||
/// c'tor
|
||||
explicit AuthInfo(const ServerSettings& settings);
|
||||
explicit AuthInfo(ServerSettings::Authorization settings);
|
||||
// explicit AuthInfo(std::string authheader);
|
||||
/// d'tor
|
||||
virtual ~AuthInfo() = default;
|
||||
|
@ -80,14 +80,14 @@ public:
|
|||
/// Authenticate with basic scheme
|
||||
ErrorCode authenticateBasic(const std::string& credentials);
|
||||
/// Authenticate with bearer scheme
|
||||
ErrorCode authenticateBearer(const std::string& token);
|
||||
// ErrorCode authenticateBearer(const std::string& token);
|
||||
/// Authenticate with basic or bearer scheme with an auth header
|
||||
ErrorCode authenticate(const std::string& auth);
|
||||
/// Authenticate with scheme ("basic" or "bearer") and auth param
|
||||
ErrorCode authenticate(const std::string& scheme, const std::string& param);
|
||||
|
||||
/// @return JWS token for @p username and @p password
|
||||
ErrorOr<std::string> getToken(const std::string& username, const std::string& password) const;
|
||||
// ErrorOr<std::string> getToken(const std::string& username, const std::string& password) const;
|
||||
/// @return if the authenticated user has permission to access @p ressource
|
||||
bool hasPermission(const std::string& resource) const;
|
||||
|
||||
|
@ -99,7 +99,7 @@ private:
|
|||
/// optional token expiration
|
||||
std::optional<std::chrono::system_clock::time_point> expires_;
|
||||
/// server configuration
|
||||
ServerSettings settings_;
|
||||
ServerSettings::Authorization settings_;
|
||||
|
||||
/// Validate @p username and @p password
|
||||
/// @return true if username and password are correct
|
||||
|
|
|
@ -53,9 +53,9 @@ Request::Request(const Server& server, const std::string& method) : server_(serv
|
|||
|
||||
bool Request::hasPermission(const AuthInfo& authinfo) const
|
||||
{
|
||||
std::ignore = authinfo;
|
||||
return true;
|
||||
// return authinfo.hasPermission(method_);
|
||||
bool has_permission = authinfo.hasPermission(method());
|
||||
LOG(INFO, LOG_TAG) << "Has permission for '" << method() << "': " << has_permission << "\n";
|
||||
return has_permission;
|
||||
}
|
||||
|
||||
const std::string& Request::method() const
|
||||
|
@ -109,7 +109,7 @@ ControlRequestFactory::ControlRequestFactory(const Server& server)
|
|||
add_request(std::make_shared<ServerGetStatusRequest>(server));
|
||||
add_request(std::make_shared<ServerDeleteClientRequest>(server));
|
||||
add_request(std::make_shared<ServerAuthenticateRequest>(server));
|
||||
add_request(std::make_shared<ServerGetTokenRequest>(server));
|
||||
// add_request(std::make_shared<ServerGetTokenRequest>(server));
|
||||
}
|
||||
|
||||
|
||||
|
@ -877,7 +877,7 @@ void ServerAuthenticateRequest::execute(const jsonrpcpp::request_ptr& request, A
|
|||
on_response(std::move(response), nullptr);
|
||||
}
|
||||
|
||||
|
||||
#if 0
|
||||
ServerGetTokenRequest::ServerGetTokenRequest(const Server& server) : Request(server, "Server.GetToken")
|
||||
{
|
||||
}
|
||||
|
@ -911,3 +911,4 @@ void ServerGetTokenRequest::execute(const jsonrpcpp::request_ptr& request, AuthI
|
|||
|
||||
on_response(std::move(response), nullptr);
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -312,6 +312,7 @@ public:
|
|||
};
|
||||
|
||||
|
||||
#if 0
|
||||
/// "Server.GetToken" request
|
||||
class ServerGetTokenRequest : public Request
|
||||
{
|
||||
|
@ -320,3 +321,4 @@ public:
|
|||
explicit ServerGetTokenRequest(const Server& server);
|
||||
void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override;
|
||||
};
|
||||
#endif
|
||||
|
|
|
@ -59,7 +59,7 @@ class ControlSession : public std::enable_shared_from_this<ControlSession>
|
|||
{
|
||||
public:
|
||||
/// ctor. Received message from the client are passed to ControlMessageReceiver
|
||||
ControlSession(ControlMessageReceiver* receiver, const ServerSettings& settings) : authinfo(settings), message_receiver_(receiver)
|
||||
ControlSession(ControlMessageReceiver* receiver, const ServerSettings& settings) : authinfo(settings.auth), message_receiver_(receiver)
|
||||
{
|
||||
}
|
||||
virtual ~ControlSession() = default;
|
||||
|
|
|
@ -448,6 +448,7 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe
|
|||
auto authheader = req_[beast::http::field::authorization];
|
||||
if (!authheader.empty())
|
||||
{
|
||||
LOG(INFO, LOG_TAG) << "Auth header: " << authheader << "\n";
|
||||
auto ec = authinfo.authenticate(std::string{authheader});
|
||||
if (ec)
|
||||
{
|
||||
|
|
|
@ -137,9 +137,22 @@ void Server::processRequest(const jsonrpcpp::request_ptr& request, AuthInfo& aut
|
|||
if (req)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (req->hasPermission(authinfo))
|
||||
{
|
||||
req->execute(request, authinfo, on_response);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::optional<jsonrpcpp::RequestException> e;
|
||||
if (!authinfo.hasAuthInfo())
|
||||
e.emplace(jsonrpcpp::Error("Unauthorized", 401), request->id());
|
||||
else
|
||||
e.emplace(jsonrpcpp::Error("Forbidden", 403), request->id());
|
||||
auto response = std::make_shared<jsonrpcpp::RequestException>(e.value());
|
||||
on_response(std::move(response), nullptr);
|
||||
}
|
||||
}
|
||||
catch (const jsonrpcpp::RequestException& e)
|
||||
{
|
||||
LOG(ERROR, LOG_TAG) << "Server::onMessageReceived JsonRequestException: " << e.to_json().dump() << ", message: " << request->to_json().dump()
|
||||
|
@ -189,7 +202,7 @@ void Server::onMessageReceived(std::shared_ptr<ControlSession> controlSession, c
|
|||
{
|
||||
jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
|
||||
processRequest(request, controlSession->authinfo,
|
||||
[this, controlSession, response_handler](jsonrpcpp::entity_ptr response, jsonrpcpp::notification_ptr notification)
|
||||
[this, controlSession, response_handler](const jsonrpcpp::entity_ptr& response, const jsonrpcpp::notification_ptr& notification)
|
||||
{
|
||||
// if (controlSession->authinfo.hasAuthInfo())
|
||||
// {
|
||||
|
@ -226,8 +239,8 @@ void Server::onMessageReceived(std::shared_ptr<ControlSession> controlSession, c
|
|||
{
|
||||
jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(batch_entity);
|
||||
processRequest(request, controlSession->authinfo,
|
||||
[controlSession, response_handler, &responseBatch, ¬ificationBatch](jsonrpcpp::entity_ptr response,
|
||||
jsonrpcpp::notification_ptr notification)
|
||||
[controlSession, response_handler, &responseBatch, ¬ificationBatch](const jsonrpcpp::entity_ptr& response,
|
||||
const jsonrpcpp::notification_ptr& notification)
|
||||
{
|
||||
if (response != nullptr)
|
||||
responseBatch.add_ptr(response);
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
// standard headers
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
@ -67,26 +68,57 @@ struct ServerSettings
|
|||
}
|
||||
};
|
||||
|
||||
/// Authorization settings
|
||||
struct Authorization
|
||||
{
|
||||
/// Role settings
|
||||
struct Role
|
||||
{
|
||||
/// c'tor
|
||||
Role() = default;
|
||||
|
||||
/// c'tor
|
||||
explicit Role(const std::string& role_permissions)
|
||||
{
|
||||
std::string perm;
|
||||
role = utils::string::split_left(role_permissions, ':', perm);
|
||||
permissions = utils::string::split(perm, ',');
|
||||
}
|
||||
|
||||
/// role name
|
||||
std::string role;
|
||||
/// permissions
|
||||
std::vector<std::string> permissions;
|
||||
};
|
||||
|
||||
/// User settings
|
||||
struct User
|
||||
{
|
||||
/// c'tor
|
||||
explicit User(const std::string& user_permissions_password)
|
||||
explicit User(const std::string& user_password_role)
|
||||
{
|
||||
std::string perm;
|
||||
name = utils::string::split_left(user_permissions_password, ':', perm);
|
||||
perm = utils::string::split_left(perm, ':', password);
|
||||
permissions = utils::string::split(perm, ',');
|
||||
name = utils::string::split_left(user_password_role, ':', perm);
|
||||
password = utils::string::split_left(perm, ':', role_name);
|
||||
}
|
||||
|
||||
/// user name
|
||||
std::string name;
|
||||
/// permissions
|
||||
std::vector<std::string> permissions;
|
||||
/// password
|
||||
std::string password;
|
||||
/// role
|
||||
std::string role_name;
|
||||
/// role
|
||||
std::shared_ptr<Role> role;
|
||||
};
|
||||
|
||||
/// is auth enabled
|
||||
bool enabled{false};
|
||||
/// users
|
||||
std::vector<User> users;
|
||||
/// roles
|
||||
std::vector<std::shared_ptr<Role>> roles;
|
||||
};
|
||||
|
||||
/// HTTP settings
|
||||
struct Http
|
||||
|
@ -163,7 +195,7 @@ struct ServerSettings
|
|||
|
||||
Server server; ///< Server settings
|
||||
Ssl ssl; ///< SSL settings
|
||||
std::vector<User> users; ///< User settings
|
||||
Authorization auth; ///< Auth settings
|
||||
Http http; ///< HTTP settings
|
||||
Tcp tcp; ///< TCP settings
|
||||
Stream stream; ///< Stream settings
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
|
||||
// local headers
|
||||
#include "common/popl.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#ifdef HAS_DAEMON
|
||||
#include "common/daemon.hpp"
|
||||
|
@ -90,10 +92,10 @@ int main(int argc, char* argv[])
|
|||
auto client_cert_opt =
|
||||
conf.add<Value<std::filesystem::path>>("", "ssl.client_cert", "List of client CA certificate files, can be configured multiple times", "");
|
||||
|
||||
#if 0 // feature: users
|
||||
// Users setting
|
||||
auto users_value = conf.add<Value<string>>("", "users.user", "<User nane>:<permissions>:<password>");
|
||||
#endif
|
||||
conf.add<Value<bool>>("", "authorization.enabled", "enabled authorization", settings.auth.enabled, &settings.auth.enabled);
|
||||
auto roles_value = conf.add<Value<string>>("", "authorization.role", "<Role name>:<permissions>");
|
||||
auto users_value = conf.add<Value<string>>("", "authorization.user", "<User nane>:<password>:<role>");
|
||||
|
||||
// HTTP RPC settings
|
||||
conf.add<Value<bool>>("", "http.enabled", "enable HTTP Json RPC (HTTP POST and websockets)", settings.http.enabled, &settings.http.enabled);
|
||||
|
@ -320,15 +322,37 @@ int main(int argc, char* argv[])
|
|||
settings.stream.sources.push_back(sourceValue->value(n));
|
||||
}
|
||||
|
||||
#if 0 // feature: users
|
||||
if (settings.auth.enabled)
|
||||
{
|
||||
for (size_t n = 0; n < roles_value->count(); ++n)
|
||||
{
|
||||
settings.auth.roles.emplace_back(std::make_shared<ServerSettings::Authorization::Role>(roles_value->value(n)));
|
||||
const auto& role = settings.auth.roles.back();
|
||||
LOG(DEBUG, LOG_TAG) << "Role: " << role->role << ", permissions: " << utils::string::container_to_string(role->permissions) << "\n";
|
||||
}
|
||||
|
||||
auto empty_role = std::make_shared<ServerSettings::Authorization::Role>();
|
||||
for (size_t n = 0; n < users_value->count(); ++n)
|
||||
{
|
||||
settings.users.emplace_back(users_value->value(n));
|
||||
LOG(DEBUG, LOG_TAG) << "User: " << settings.users.back().name
|
||||
<< ", permissions: " << utils::string::container_to_string(settings.users.back().permissions)
|
||||
<< ", pw: " << settings.users.back().password << "\n";
|
||||
settings.auth.users.emplace_back(users_value->value(n));
|
||||
ServerSettings::Authorization::User& user = settings.auth.users.back();
|
||||
if (user.role_name.empty())
|
||||
{
|
||||
user.role = empty_role;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto& role_iter =
|
||||
find_if(settings.auth.roles.begin(), settings.auth.roles.end(), [&](const auto& role) { return role->role == user.role_name; });
|
||||
if (role_iter != settings.auth.roles.end())
|
||||
user.role = *role_iter;
|
||||
}
|
||||
if (user.role == nullptr)
|
||||
throw SnapException("Role '" + user.role_name + "' for user '" + user.name + "' not found");
|
||||
|
||||
LOG(DEBUG, LOG_TAG) << "User: " << user.name << ", pw: " << user.password << ", role: " << user.role_name << "\n";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef HAS_DAEMON
|
||||
std::unique_ptr<Daemon> daemon;
|
||||
|
|
|
@ -20,7 +20,7 @@ set(TEST_SOURCES
|
|||
${CMAKE_SOURCE_DIR}/common/base64.cpp
|
||||
${CMAKE_SOURCE_DIR}/common/utils/string_utils.cpp
|
||||
${CMAKE_SOURCE_DIR}/server/authinfo.cpp
|
||||
${CMAKE_SOURCE_DIR}/server/jwt.cpp
|
||||
# ${CMAKE_SOURCE_DIR}/server/jwt.cpp
|
||||
${CMAKE_SOURCE_DIR}/server/streamreader/control_error.cpp
|
||||
${CMAKE_SOURCE_DIR}/server/streamreader/properties.cpp
|
||||
${CMAKE_SOURCE_DIR}/server/streamreader/metadata.cpp)
|
||||
|
|
|
@ -19,14 +19,10 @@
|
|||
// prototype/interface header file
|
||||
|
||||
// local headers
|
||||
#include "common/aixlog.hpp"
|
||||
#include "common/base64.h"
|
||||
#include "common/error_code.hpp"
|
||||
#include "common/stream_uri.hpp"
|
||||
#include "common/utils/string_utils.hpp"
|
||||
#include "server/authinfo.hpp"
|
||||
#include "server/jwt.hpp"
|
||||
#include "server/server_settings.hpp"
|
||||
// #include "server/jwt.hpp"
|
||||
#include "server/streamreader/control_error.hpp"
|
||||
#include "server/streamreader/properties.hpp"
|
||||
|
||||
|
@ -34,7 +30,7 @@
|
|||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
// standard headers
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <regex>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
@ -91,9 +87,9 @@ TEST_CASE("String utils")
|
|||
}
|
||||
|
||||
|
||||
#if 0
|
||||
TEST_CASE("JWT")
|
||||
{
|
||||
#if 0
|
||||
/// ECDSA
|
||||
{
|
||||
const auto* key = "-----BEGIN EC PRIVATE KEY-----\n"
|
||||
|
@ -137,7 +133,6 @@ TEST_CASE("JWT")
|
|||
REQUIRE(jwt.getIat().value() == std::chrono::seconds(1516239022));
|
||||
REQUIRE(!jwt.getExp().has_value());
|
||||
}
|
||||
#endif
|
||||
|
||||
/// RSA keys
|
||||
{
|
||||
|
@ -220,6 +215,7 @@ TEST_CASE("JWT")
|
|||
REQUIRE(!jwt.getExp().has_value());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
TEST_CASE("Uri")
|
||||
|
@ -688,7 +684,7 @@ TEST_CASE("WildcardMatch")
|
|||
REQUIRE(!wildcardMatch("*get*erver*", "Server.getToken"));
|
||||
}
|
||||
|
||||
|
||||
#if 0
|
||||
TEST_CASE("Auth")
|
||||
{
|
||||
{
|
||||
|
@ -735,3 +731,4 @@ TEST_CASE("Auth")
|
|||
REQUIRE(!auth.hasPermission("stream"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue