Read users and roles, remove JWT

This commit is contained in:
badaix 2025-02-01 21:01:39 +01:00 committed by Johannes Pohl
parent 9247588764
commit 42f4c39e6c
13 changed files with 144 additions and 68 deletions

View file

@ -103,7 +103,7 @@ struct ClientSettings
/// Log settings /// Log settings
struct Logging struct Logging
{ {
/// The log sink (null,system,stdout,stderr,file:<filename>) /// The log sink (null,system,stdout,stderr,file)
std::string sink; std::string sink;
/// Log filter /// Log filter
std::string filter{"*:info"}; std::string filter{"*:info"};

View file

@ -6,7 +6,7 @@ set(SERVER_SOURCES
control_session_tcp.cpp control_session_tcp.cpp
control_session_http.cpp control_session_http.cpp
control_session_ws.cpp control_session_ws.cpp
jwt.cpp # jwt.cpp
snapserver.cpp snapserver.cpp
server.cpp server.cpp
stream_server.cpp stream_server.cpp

View file

@ -23,14 +23,12 @@
#include "common/aixlog.hpp" #include "common/aixlog.hpp"
#include "common/base64.h" #include "common/base64.h"
#include "common/utils/string_utils.hpp" #include "common/utils/string_utils.hpp"
#include "jwt.hpp"
// 3rd party headers // 3rd party headers
// standard headers // standard headers
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <fstream>
#include <string> #include <string>
#include <system_error> #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 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()) if (iter == settings_.users.end())
return ErrorCode{AuthErrc::unknown_user}; return ErrorCode{AuthErrc::unknown_user};
if (password.has_value() && (iter->password != password.value())) 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 scheme_normed = utils::string::trim_copy(utils::string::tolower_copy(scheme));
std::string param_normed = utils::string::trim_copy(param); std::string param_normed = utils::string::trim_copy(param);
if (scheme_normed == "bearer") // if (scheme_normed == "bearer")
return authenticateBearer(param_normed); // return authenticateBearer(param_normed);
else if (scheme_normed == "basic") if (scheme_normed == "basic")
return authenticateBasic(param_normed); 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; return ec;
} }
#if 0
ErrorCode AuthInfo::authenticateBearer(const std::string& token) ErrorCode AuthInfo::authenticateBearer(const std::string& token)
{ {
has_auth_info_ = false; 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 ErrorCode{AuthErrc::failed_to_create_token};
return token.value(); return token.value();
} }
#endif
bool AuthInfo::isExpired() const bool AuthInfo::isExpired() const
@ -236,19 +236,25 @@ const std::string& AuthInfo::username() const
bool AuthInfo::hasPermission(const std::string& resource) const bool AuthInfo::hasPermission(const std::string& resource) const
{ {
if (!settings_.enabled)
return true;
if (!hasAuthInfo()) if (!hasAuthInfo())
return false; return false;
auto iter = std::find_if(settings_.users.begin(), settings_.users.end(), [&](const ServerSettings::User& user) { return user.name == username_; }); const auto& user_iter = std::find_if(settings_.users.begin(), settings_.users.end(), [&](const auto& user) { return user.name == username_; });
if (iter == settings_.users.end()) if (user_iter == settings_.users.end())
return false; 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); }); [&](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"; LOG(DEBUG, LOG_TAG) << "Found permission for ressource '" << resource << "': '" << *perm_iter << "'\n";
return true; return true;
} }
return false; return false;
} }

View file

@ -1,6 +1,6 @@
/*** /***
This file is part of snapcast 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 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 it under the terms of the GNU General Public License as published by
@ -66,7 +66,7 @@ class AuthInfo
{ {
public: public:
/// c'tor /// c'tor
explicit AuthInfo(const ServerSettings& settings); explicit AuthInfo(ServerSettings::Authorization settings);
// explicit AuthInfo(std::string authheader); // explicit AuthInfo(std::string authheader);
/// d'tor /// d'tor
virtual ~AuthInfo() = default; virtual ~AuthInfo() = default;
@ -80,14 +80,14 @@ public:
/// Authenticate with basic scheme /// Authenticate with basic scheme
ErrorCode authenticateBasic(const std::string& credentials); ErrorCode authenticateBasic(const std::string& credentials);
/// Authenticate with bearer scheme /// 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 /// Authenticate with basic or bearer scheme with an auth header
ErrorCode authenticate(const std::string& auth); ErrorCode authenticate(const std::string& auth);
/// Authenticate with scheme ("basic" or "bearer") and auth param /// Authenticate with scheme ("basic" or "bearer") and auth param
ErrorCode authenticate(const std::string& scheme, const std::string& param); ErrorCode authenticate(const std::string& scheme, const std::string& param);
/// @return JWS token for @p username and @p password /// @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 /// @return if the authenticated user has permission to access @p ressource
bool hasPermission(const std::string& resource) const; bool hasPermission(const std::string& resource) const;
@ -99,7 +99,7 @@ private:
/// optional token expiration /// optional token expiration
std::optional<std::chrono::system_clock::time_point> expires_; std::optional<std::chrono::system_clock::time_point> expires_;
/// server configuration /// server configuration
ServerSettings settings_; ServerSettings::Authorization settings_;
/// Validate @p username and @p password /// Validate @p username and @p password
/// @return true if username and password are correct /// @return true if username and password are correct

View file

@ -53,9 +53,9 @@ Request::Request(const Server& server, const std::string& method) : server_(serv
bool Request::hasPermission(const AuthInfo& authinfo) const bool Request::hasPermission(const AuthInfo& authinfo) const
{ {
std::ignore = authinfo; bool has_permission = authinfo.hasPermission(method());
return true; LOG(INFO, LOG_TAG) << "Has permission for '" << method() << "': " << has_permission << "\n";
// return authinfo.hasPermission(method_); return has_permission;
} }
const std::string& Request::method() const 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<ServerGetStatusRequest>(server));
add_request(std::make_shared<ServerDeleteClientRequest>(server)); add_request(std::make_shared<ServerDeleteClientRequest>(server));
add_request(std::make_shared<ServerAuthenticateRequest>(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); on_response(std::move(response), nullptr);
} }
#if 0
ServerGetTokenRequest::ServerGetTokenRequest(const Server& server) : Request(server, "Server.GetToken") 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); on_response(std::move(response), nullptr);
} }
#endif

View file

@ -312,6 +312,7 @@ public:
}; };
#if 0
/// "Server.GetToken" request /// "Server.GetToken" request
class ServerGetTokenRequest : public Request class ServerGetTokenRequest : public Request
{ {
@ -320,3 +321,4 @@ public:
explicit ServerGetTokenRequest(const Server& server); explicit ServerGetTokenRequest(const Server& server);
void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override; void execute(const jsonrpcpp::request_ptr& request, AuthInfo& authinfo, const OnResponse& on_response) override;
}; };
#endif

View file

@ -59,7 +59,7 @@ class ControlSession : public std::enable_shared_from_this<ControlSession>
{ {
public: public:
/// ctor. Received message from the client are passed to ControlMessageReceiver /// 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; virtual ~ControlSession() = default;

View file

@ -448,6 +448,7 @@ void ControlSessionHttp::on_read(beast::error_code ec, std::size_t bytes_transfe
auto authheader = req_[beast::http::field::authorization]; auto authheader = req_[beast::http::field::authorization];
if (!authheader.empty()) if (!authheader.empty())
{ {
LOG(INFO, LOG_TAG) << "Auth header: " << authheader << "\n";
auto ec = authinfo.authenticate(std::string{authheader}); auto ec = authinfo.authenticate(std::string{authheader});
if (ec) if (ec)
{ {

View file

@ -138,7 +138,20 @@ void Server::processRequest(const jsonrpcpp::request_ptr& request, AuthInfo& aut
{ {
try try
{ {
req->execute(request, authinfo, on_response); 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) catch (const jsonrpcpp::RequestException& e)
{ {
@ -189,7 +202,7 @@ void Server::onMessageReceived(std::shared_ptr<ControlSession> controlSession, c
{ {
jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity); jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(entity);
processRequest(request, controlSession->authinfo, 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()) // 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); jsonrpcpp::request_ptr request = dynamic_pointer_cast<jsonrpcpp::Request>(batch_entity);
processRequest(request, controlSession->authinfo, processRequest(request, controlSession->authinfo,
[controlSession, response_handler, &responseBatch, &notificationBatch](jsonrpcpp::entity_ptr response, [controlSession, response_handler, &responseBatch, &notificationBatch](const jsonrpcpp::entity_ptr& response,
jsonrpcpp::notification_ptr notification) const jsonrpcpp::notification_ptr& notification)
{ {
if (response != nullptr) if (response != nullptr)
responseBatch.add_ptr(response); responseBatch.add_ptr(response);

View file

@ -25,6 +25,7 @@
// standard headers // standard headers
#include <cstdint> #include <cstdint>
#include <filesystem> #include <filesystem>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@ -67,27 +68,58 @@ struct ServerSettings
} }
}; };
/// User settings /// Authorization settings
struct User struct Authorization
{ {
/// c'tor /// Role settings
explicit User(const std::string& user_permissions_password) struct Role
{ {
std::string perm; /// c'tor
name = utils::string::split_left(user_permissions_password, ':', perm); Role() = default;
perm = utils::string::split_left(perm, ':', password);
permissions = utils::string::split(perm, ',');
}
/// user name /// c'tor
std::string name; explicit Role(const std::string& role_permissions)
/// permissions {
std::vector<std::string> permissions; std::string perm;
/// password role = utils::string::split_left(role_permissions, ':', perm);
std::string password; 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_password_role)
{
std::string perm;
name = utils::string::split_left(user_password_role, ':', perm);
password = utils::string::split_left(perm, ':', role_name);
}
/// user name
std::string name;
/// 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 /// HTTP settings
struct Http struct Http
{ {
@ -163,7 +195,7 @@ struct ServerSettings
Server server; ///< Server settings Server server; ///< Server settings
Ssl ssl; ///< SSL settings Ssl ssl; ///< SSL settings
std::vector<User> users; ///< User settings Authorization auth; ///< Auth settings
Http http; ///< HTTP settings Http http; ///< HTTP settings
Tcp tcp; ///< TCP settings Tcp tcp; ///< TCP settings
Stream stream; ///< Stream settings Stream stream; ///< Stream settings

View file

@ -18,6 +18,8 @@
// local headers // local headers
#include "common/popl.hpp" #include "common/popl.hpp"
#include <algorithm>
#include <cstddef>
#include <filesystem> #include <filesystem>
#ifdef HAS_DAEMON #ifdef HAS_DAEMON
#include "common/daemon.hpp" #include "common/daemon.hpp"
@ -90,10 +92,10 @@ int main(int argc, char* argv[])
auto client_cert_opt = auto client_cert_opt =
conf.add<Value<std::filesystem::path>>("", "ssl.client_cert", "List of client CA certificate files, can be configured multiple times", ""); 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
// Users setting conf.add<Value<bool>>("", "authorization.enabled", "enabled authorization", settings.auth.enabled, &settings.auth.enabled);
auto users_value = conf.add<Value<string>>("", "users.user", "<User nane>:<permissions>:<password>"); auto roles_value = conf.add<Value<string>>("", "authorization.role", "<Role name>:<permissions>");
#endif auto users_value = conf.add<Value<string>>("", "authorization.user", "<User nane>:<password>:<role>");
// HTTP RPC settings // HTTP RPC settings
conf.add<Value<bool>>("", "http.enabled", "enable HTTP Json RPC (HTTP POST and websockets)", settings.http.enabled, &settings.http.enabled); 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)); settings.stream.sources.push_back(sourceValue->value(n));
} }
#if 0 // feature: users if (settings.auth.enabled)
for (size_t n = 0; n < users_value->count(); ++n)
{ {
settings.users.emplace_back(users_value->value(n)); for (size_t n = 0; n < roles_value->count(); ++n)
LOG(DEBUG, LOG_TAG) << "User: " << settings.users.back().name {
<< ", permissions: " << utils::string::container_to_string(settings.users.back().permissions) settings.auth.roles.emplace_back(std::make_shared<ServerSettings::Authorization::Role>(roles_value->value(n)));
<< ", pw: " << settings.users.back().password << "\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.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 #ifdef HAS_DAEMON
std::unique_ptr<Daemon> daemon; std::unique_ptr<Daemon> daemon;

View file

@ -20,7 +20,7 @@ set(TEST_SOURCES
${CMAKE_SOURCE_DIR}/common/base64.cpp ${CMAKE_SOURCE_DIR}/common/base64.cpp
${CMAKE_SOURCE_DIR}/common/utils/string_utils.cpp ${CMAKE_SOURCE_DIR}/common/utils/string_utils.cpp
${CMAKE_SOURCE_DIR}/server/authinfo.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/control_error.cpp
${CMAKE_SOURCE_DIR}/server/streamreader/properties.cpp ${CMAKE_SOURCE_DIR}/server/streamreader/properties.cpp
${CMAKE_SOURCE_DIR}/server/streamreader/metadata.cpp) ${CMAKE_SOURCE_DIR}/server/streamreader/metadata.cpp)

View file

@ -19,14 +19,10 @@
// prototype/interface header file // prototype/interface header file
// local headers // local headers
#include "common/aixlog.hpp"
#include "common/base64.h"
#include "common/error_code.hpp" #include "common/error_code.hpp"
#include "common/stream_uri.hpp" #include "common/stream_uri.hpp"
#include "common/utils/string_utils.hpp" #include "common/utils/string_utils.hpp"
#include "server/authinfo.hpp" // #include "server/jwt.hpp"
#include "server/jwt.hpp"
#include "server/server_settings.hpp"
#include "server/streamreader/control_error.hpp" #include "server/streamreader/control_error.hpp"
#include "server/streamreader/properties.hpp" #include "server/streamreader/properties.hpp"
@ -34,7 +30,7 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
// standard headers // standard headers
#include <chrono> #include <iostream>
#include <regex> #include <regex>
#include <system_error> #include <system_error>
#include <vector> #include <vector>
@ -91,9 +87,9 @@ TEST_CASE("String utils")
} }
#if 0
TEST_CASE("JWT") TEST_CASE("JWT")
{ {
#if 0
/// ECDSA /// ECDSA
{ {
const auto* key = "-----BEGIN EC PRIVATE KEY-----\n" 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.getIat().value() == std::chrono::seconds(1516239022));
REQUIRE(!jwt.getExp().has_value()); REQUIRE(!jwt.getExp().has_value());
} }
#endif
/// RSA keys /// RSA keys
{ {
@ -220,6 +215,7 @@ TEST_CASE("JWT")
REQUIRE(!jwt.getExp().has_value()); REQUIRE(!jwt.getExp().has_value());
} }
} }
#endif
TEST_CASE("Uri") TEST_CASE("Uri")
@ -688,7 +684,7 @@ TEST_CASE("WildcardMatch")
REQUIRE(!wildcardMatch("*get*erver*", "Server.getToken")); REQUIRE(!wildcardMatch("*get*erver*", "Server.getToken"));
} }
#if 0
TEST_CASE("Auth") TEST_CASE("Auth")
{ {
{ {
@ -735,3 +731,4 @@ TEST_CASE("Auth")
REQUIRE(!auth.hasPermission("stream")); REQUIRE(!auth.hasPermission("stream"));
} }
} }
#endif