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
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"};

View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

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];
if (!authheader.empty())
{
LOG(INFO, LOG_TAG) << "Auth header: " << authheader << "\n";
auto ec = authinfo.authenticate(std::string{authheader});
if (ec)
{

View file

@ -138,7 +138,20 @@ void Server::processRequest(const jsonrpcpp::request_ptr& request, AuthInfo& aut
{
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)
{
@ -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, &notificationBatch](jsonrpcpp::entity_ptr response,
jsonrpcpp::notification_ptr notification)
[controlSession, response_handler, &responseBatch, &notificationBatch](const jsonrpcpp::entity_ptr& response,
const jsonrpcpp::notification_ptr& notification)
{
if (response != nullptr)
responseBatch.add_ptr(response);

View file

@ -25,6 +25,7 @@
// standard headers
#include <cstdint>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
@ -67,27 +68,58 @@ struct ServerSettings
}
};
/// User settings
struct User
/// Authorization settings
struct Authorization
{
/// c'tor
explicit User(const std::string& user_permissions_password)
/// Role settings
struct 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, ',');
}
/// c'tor
Role() = default;
/// user name
std::string name;
/// permissions
std::vector<std::string> permissions;
/// password
std::string password;
/// 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_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
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

View file

@ -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
// Users setting
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
for (size_t n = 0; n < users_value->count(); ++n)
if (settings.auth.enabled)
{
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";
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.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;

View file

@ -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)

View file

@ -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