diff --git a/cmake/UseODB.cmake b/cmake/UseODB.cmake index ebef8c110..7f9d2e3ab 100644 --- a/cmake/UseODB.cmake +++ b/cmake/UseODB.cmake @@ -175,8 +175,9 @@ function(odb_compile outvar) list(APPEND ODB_ARGS "-I${dir}") endforeach() - file(REMOVE_RECURSE "${ODB_COMPILE_OUTPUT_DIR}") - file(MAKE_DIRECTORY "${ODB_COMPILE_OUTPUT_DIR}") + if(NOT EXISTS "${ODB_COMPILE_OUTPUT_DIR}") + file(MAKE_DIRECTORY "${ODB_COMPILE_OUTPUT_DIR}") + endif() foreach(input ${PARAM_FILES}) get_filename_component(fname "${input}" NAME_WE) diff --git a/src/core/audit/AuditFactory.cpp b/src/core/audit/AuditFactory.cpp index 063fe30e2..7a16d7ac7 100644 --- a/src/core/audit/AuditFactory.cpp +++ b/src/core/audit/AuditFactory.cpp @@ -143,8 +143,6 @@ IAuthEventPtr Factory::AuthEvent(const DBPtr &database, Cred::ICredentialPtr cre ASSERT_LOG(database, "Database cannot be null."); ASSERT_LOG(credential, "Credential must be non null."); ASSERT_LOG(!door.empty(), "Door must be set."); - ASSERT_LOG(parent, "Parent must be non null."); - ASSERT_LOG(parent->id(), "Parent must be already persisted."); AuditEntryPtr parent_odb; if (parent && parent->id()) diff --git a/src/core/audit/AuditFwd.hpp b/src/core/audit/AuditFwd.hpp index a582ca95b..778bd172a 100644 --- a/src/core/audit/AuditFwd.hpp +++ b/src/core/audit/AuditFwd.hpp @@ -124,80 +124,65 @@ using ZoneEventPtr = std::shared_ptr; enum class EventType { - /** - * Admin & System Event Types - */ - AL_SYSTEM = 0x1000, - /** - * User Event Types - */ - AL_USER = 0x2000, - - WSAPI_CALL = 0x00 & AL_SYSTEM, - - USER_CREATED = 0x10 & AL_SYSTEM, - USER_DELETED = 0x11 & AL_SYSTEM, - USER_EDITED = 0x12 & AL_SYSTEM, - USER_PASSWORD_CHANGED = 0x13 & AL_SYSTEM, - /** - * An attempt to change the password failed. - */ - USER_PASSWORD_CHANGE_FAILURE = 0x14 & AL_SYSTEM, - - GROUP_CREATED = 0x20 & AL_SYSTEM, - GROUP_UPDATED = 0x21 & AL_SYSTEM, - GROUP_DELETED = 0x22 & AL_SYSTEM, - /** - * Someone joined the group. - */ - GROUP_MEMBERSHIP_JOINED = 0x23 & AL_SYSTEM, - /** - * Someone left the group. - */ - GROUP_MEMBERSHIP_LEFT = 0x24 & AL_SYSTEM, - - CREDENTIAL_DELETED = 0x30 & AL_SYSTEM, - CREDENTIAL_CREATED = 0x31 & AL_SYSTEM, - CREDENTIAL_UPDATED = 0x32 & AL_SYSTEM, - - SCHEDULE_CREATED = 0x40 & AL_SYSTEM, - SCHEDULE_DELETED = 0x41 & AL_SYSTEM, - SCHEDULE_UPDATED = 0x42 & AL_SYSTEM, - - DOOR_CREATED = 0x50 & AL_SYSTEM, - DOOR_UPDATED = 0x51 & AL_SYSTEM, - DOOR_DELETED = 0x52 & AL_SYSTEM, - /** - * This event is linked to door. It is generated - * by the Schedule CRUD manager. It's used to let a door - * know that it may have been removed from a mapping. - * - * Fixme: Currently this event is triggered for every door in a mapping - * everytime a schedule is updated. - */ - MAPPING_MAY_HAVE_CHANGED = 0x53 & AL_SYSTEM, - - ACCESS_POINT_CREATED = 0x60 & AL_SYSTEM, - ACCESS_POINT_UPDATED = 0x61 & AL_SYSTEM, - ACCESS_POINT_DELETED = 0x62 & AL_SYSTEM, - - ZONE_CREATED = 0x70 & AL_SYSTEM, - ZONE_UPDATED = 0x71 & AL_SYSTEM, - ZONE_DELETED = 0x72 & AL_SYSTEM, - - UPDATE_CREATED = 0x80 & AL_SYSTEM, - UPDATE_ACKED = 0x81 & AL_SYSTEM, - UPDATE_CANCELLED = 0x81 & AL_SYSTEM, - - DOOR_OPENED = 0x10 & AL_USER, - DOOR_OPENED_MANUALLY = 0x11 & AL_USER, - DOOR_FORCED = 0x12 & AL_USER, - DOOR_FORCED_END = 0x13 & AL_USER, - - AUTH_GRANTED = 0x20 & AL_USER, - AUTH_DENIED = 0x21 & AL_USER, - - LAST__ = 0xffff + // 0..63 reserved to keep masks within 64-bit numeric serialization + + // 0..15 System/Admin + WSAPI_CALL = 0, + USER_CREATED = 1, + USER_DELETED = 2, + USER_EDITED = 3, + USER_PASSWORD_CHANGED = 4, + USER_PASSWORD_CHANGE_FAILURE = 5, + + // 16..23 Groups + GROUP_CREATED = 16, + GROUP_UPDATED = 17, + GROUP_DELETED = 18, + GROUP_MEMBERSHIP_JOINED = 19, + GROUP_MEMBERSHIP_LEFT = 20, + + // 24..27 Credentials + CREDENTIAL_CREATED = 24, + CREDENTIAL_UPDATED = 25, + CREDENTIAL_DELETED = 26, + + // 28..30 Schedules + SCHEDULE_CREATED = 28, + SCHEDULE_UPDATED = 29, + SCHEDULE_DELETED = 30, + + // 31..35 Doors + DOOR_CREATED = 31, + DOOR_UPDATED = 32, + DOOR_DELETED = 33, + MAPPING_MAY_HAVE_CHANGED = 34, + + // 36..38 Access points + ACCESS_POINT_CREATED = 36, + ACCESS_POINT_UPDATED = 37, + ACCESS_POINT_DELETED = 38, + + // 39..41 Zones + ZONE_CREATED = 39, + ZONE_UPDATED = 40, + ZONE_DELETED = 41, + + // 42..44 Updates + UPDATE_CREATED = 42, + UPDATE_ACKED = 43, + UPDATE_CANCELLED = 44, + + // 45..48 Door usage + DOOR_OPENED = 45, + DOOR_OPENED_MANUALLY = 46, + DOOR_FORCED = 47, + DOOR_FORCED_END = 48, + + // 49..50 Auth outcomes + AUTH_GRANTED = 49, + AUTH_DENIED = 50, + + LAST__ = 64 }; using EventMask = FlagSet; diff --git a/src/core/auth/AuthTarget.cpp b/src/core/auth/AuthTarget.cpp index cdeab4bd6..8bd89b2ee 100644 --- a/src/core/auth/AuthTarget.cpp +++ b/src/core/auth/AuthTarget.cpp @@ -49,6 +49,11 @@ void AuthTarget::add_always_close_sched(Leosac::Tools::IScheduleCPtr const &sche always_close_.push_back(sched); } +void AuthTarget::clear_schedules() { + always_open_.clear(); + always_close_.clear(); +} + Leosac::Hardware::FGPIO *AuthTarget::gpio() const { return gpio_.get(); diff --git a/src/core/auth/AuthTarget.hpp b/src/core/auth/AuthTarget.hpp index cfc6ad87c..1dfecda93 100644 --- a/src/core/auth/AuthTarget.hpp +++ b/src/core/auth/AuthTarget.hpp @@ -57,6 +57,11 @@ class AuthTarget */ bool is_always_closed(const std::chrono::system_clock::time_point &tp) const; + /** + * Clear schedules (helper for refreshing schedules) + */ + void clear_schedules(); + /* * Reset the door the its expected default state at the current time. */ diff --git a/src/core/kernel.cpp b/src/core/kernel.cpp index 107916711..c8a5f8d7e 100644 --- a/src/core/kernel.cpp +++ b/src/core/kernel.cpp @@ -371,6 +371,7 @@ void Kernel::configure_logger() bool use_database = false; std::string syslog_min_level = "WARNING"; std::shared_ptr console; + std::string console_level = "DEBUG"; // Drop existing logger, if any. (This is for the case of a "in process" restart) spdlog::drop("syslog"); @@ -382,6 +383,7 @@ void Kernel::configure_logger() use_syslog = log_cfg_node->get("enable_syslog", true); use_database = log_cfg_node->get("enable_database", false); syslog_min_level = log_cfg_node->get("min_syslog", "WARNING"); + console_level = log_cfg_node->get("console_level", "DEBUG"); } if (use_syslog) { @@ -399,7 +401,9 @@ void Kernel::configure_logger() else console = spdlog::create( "console", {std::make_shared()}); - console->set_level(spdlog::level::debug); + + console->set_level(static_cast( + LogHelper::log_level_from_string(console_level))); } const ModuleManager &Kernel::module_manager() const diff --git a/src/modules/auth/auth-db/AuthDBFwd.hpp b/src/modules/auth/auth-db/AuthDBFwd.hpp index 787a82fd0..dcd0e08dc 100644 --- a/src/modules/auth/auth-db/AuthDBFwd.hpp +++ b/src/modules/auth/auth-db/AuthDBFwd.hpp @@ -19,13 +19,17 @@ #pragma once +#include + namespace Leosac { namespace Module { namespace Auth { +class AuthDBInstance; +using AuthDBInstancePtr = std::shared_ptr; using AuthDBInstanceId = unsigned long long; } } -} +} \ No newline at end of file diff --git a/src/modules/auth/auth-db/AuthDBInstance.cpp b/src/modules/auth/auth-db/AuthDBInstance.cpp index 695235681..d9e867700 100644 --- a/src/modules/auth/auth-db/AuthDBInstance.cpp +++ b/src/modules/auth/auth-db/AuthDBInstance.cpp @@ -18,3 +18,380 @@ */ #include "AuthDBInstance.hpp" +#include "core/CoreUtils.hpp" +#include "tools/db/DBService.hpp" +#include "tools/log.hpp" +#include "tools/Colorize.hpp" +#include "core/auth/Auth.hpp" +#include "core/auth/AuthFwd.hpp" +#include "core/auth/User.hpp" +#include "core/auth/User_odb.h" +#include "core/auth/AuthSourceBuilder.hpp" +#include "core/auth/SimpleAccessProfile.hpp" +#include "core/auth/ProfileMerger.hpp" +#include "core/credentials/ICredential.hpp" +#include "core/credentials/RFIDCard.hpp" +#include "core/credentials/PinCode.hpp" +#include "core/credentials/RFIDCardPin.hpp" +#include "core/credentials/serializers/PolymorphicCredentialSerializer.hpp" +#include "core/credentials/RFIDCard_odb.h" +#include "core/SecurityContext.hpp" +#include "core/audit/AuthEvent.hpp" +#include "core/audit/AuditFactory.hpp" +#include "exception/ExceptionsTools.hpp" +#include +#include +#include +#include + +using namespace Leosac::Module::Auth; +using namespace Leosac::Auth; +using namespace Leosac::Cred; +using namespace ::Leosac::Auth; + +AuthDBInstance::AuthDBInstance(zmqpp::context &ctx, + const std::string &auth_ctx_name, + const std::list &auth_sources_names, + const std::string &auth_target_name, + CoreUtilsPtr core_utils, + const int bits_low_threshold, + const int bits_high_threshold) + : db_service_(std::make_shared(core_utils->database())) + , bus_push_(ctx, zmqpp::socket_type::push) + , bus_sub_(ctx, zmqpp::socket_type::sub) + , name_(auth_ctx_name) + , target_name_(auth_target_name) + , core_utils_(core_utils) + , bits_low_threshold_(bits_low_threshold) + , bits_high_threshold_(bits_high_threshold) +{ + bus_push_.connect("inproc://zmq-bus-pull"); + bus_sub_.connect("inproc://zmq-bus-pub"); + bus_sub_.subscribe("KERNEL"); + + for (const auto &auth_source : auth_sources_names) { + bus_sub_.subscribe("S_" + auth_source); + } + + INFO("AuthDB Instance (" << auth_ctx_name << ") created and is subscribed to " + << boost::algorithm::join(auth_sources_names, ", ")); +} + +AuthDBInstance::~AuthDBInstance() { + INFO("AuthDBInstance (" << name_ << ") destroyed"); +} + +void AuthDBInstance::handle_bus_msg() { + zmqpp::message msg; + zmqpp::message auth_result_msg; + + bus_sub_.receive(msg); + if(handle_kernel_msg(msg)) { + return; + } + + format_auth_result_msg(auth_result_msg); + auto auth_result = handle_auth(&msg); + + if (!auth_result.ignore) { + update_and_log_auth_result_msg(auth_result, auth_result_msg); + bus_push_.send(auth_result_msg); + } +} + +bool AuthDBInstance::handle_kernel_msg(zmqpp::message &msg) { + auto msg_copy = msg.copy(); + std::string tmp; + msg_copy >> tmp; + + if (tmp == "KERNEL") { + msg_copy >> tmp; + if (tmp == "SIGHUP") { + INFO("AuthDBInstance received SIGHUP"); + // TODO: Implement reload authdb config? + } + return true; + } + return false; +} + +AuthResult AuthDBInstance::handle_auth(zmqpp::message *msg) noexcept { + AuthResult auth_result(false, false, nullptr, nullptr); + + try { + std::lock_guard guard(mutex_); + + CredResult cred_result = get_db_credentials(msg); + if (cred_result.ignore) { + auth_result.ignore = true; + return auth_result; + } + + Cred::ICredentialPtr credentials = cred_result.db_credentials; + if (!credentials) { + return auth_result; + } + + log_credentials(credentials); // Temporary + + UserPtr user = get_user(credentials); + IAccessProfilePtr profile = build_profile(user, credentials); + + if (profile) { + bool access_granted = is_access_granted(profile); + auth_result = AuthResult(access_granted, false, profile, user); + } + + log_auth_event(auth_result, credentials); + + } catch (std::exception &e) { + WARN("Error while handling auth request: " << e.what()); + log_exception(e); + } + + return auth_result; +} + +CredResult AuthDBInstance::get_db_credentials(zmqpp::message *msg) { + AuthSourceBuilder builder; + Cred::ICredentialPtr auth_source = builder.create(msg); + CredResult cred_result(false, nullptr); + + if (auto rfid_card = std::dynamic_pointer_cast(auth_source)) { + cred_result = find_credentials_by_card_id(rfid_card->card_id(), rfid_card->nb_bits()); + } else if (auto pin_code = std::dynamic_pointer_cast(auth_source)) { + INFO("Pin code auth source not supported yet"); + } else if (auto card_pin = std::dynamic_pointer_cast(auth_source)) { + INFO("Card pin auth source not supported yet"); + } else { + WARN("Unknown credential type"); + } + + return cred_result; +} + +CredResult AuthDBInstance::find_credentials_by_card_id(const std::string &card_id, const int nb_bits) const { + INFO("Searching for credentials by card id: " << card_id << " with " << nb_bits << " bits"); + + if (is_noise(nb_bits)) { + INFO("Number of bits, " << nb_bits << ", is configured as noise, ignoring."); + return CredResult(true, nullptr); + } + + try { + using namespace odb; + using namespace odb::core; + using Query = odb::query; + + auto db = core_utils_->database(); + odb::transaction t(db->begin()); + + Query q(Query::card_id == card_id && Query::nb_bits == nb_bits); + auto result = db->query(q); + + for (const auto &card : result) { + if (card.validity().is_valid()) { + t.commit(); + return CredResult(false, std::make_shared(card)); + } else { + INFO("RFIDCard is not enabled (validity check failed)."); + } + } + + t.commit(); + return CredResult(false, nullptr); + + } catch (const std::exception &e) { + WARN("Error finding credentials by card id: " << e.what()); + log_exception(e); + return CredResult(false, nullptr); + } +} + +::Leosac::Auth::UserPtr AuthDBInstance::get_user(Cred::ICredentialPtr &credentials) { + using namespace odb; + using namespace odb::core; + ::Leosac::Auth::UserPtr user = nullptr; + + auto db = core_utils_->database(); + odb::transaction t(db->begin()); + + if (auto owner_lazy = credentials->owner()) { + if (auto owner = owner_lazy.load()) { + user = owner; + } else { + INFO("User does not exist in database"); + } + } else { + INFO("These credentials do not have an owner"); + } + + t.commit(); + + return user; +} + +::Leosac::Auth::IAccessProfilePtr AuthDBInstance::build_profile(UserPtr &user, ICredentialPtr &credentials) { + try { + std::vector profiles; + + using namespace odb; + using namespace odb::core; + auto db = core_utils_->database(); + odb::transaction t(db->begin()); + + auto mappings = db->query(); + + for (const auto &schedule_map : mappings) { + if ((user && schedule_map.has_user_indirect(user)) || + (credentials && schedule_map.has_cred(credentials->id()))) { + create_profile_from_schedule_mapping(schedule_map, profiles); + } + } + + t.commit(); + + if (profiles.empty()) { + return nullptr; + } else if (profiles.size() == 1) { + return profiles.at(0); + } else { + ProfileMerger merger; + auto merged_profile = profiles.at(0); + for (size_t i = 1; i < profiles.size(); ++i) { + merged_profile = merger.merge(merged_profile, profiles.at(i)); + } + return merged_profile; + } + + } catch (const std::exception &e) { + WARN("AuthDBInstance::build_profile - Error building profile: " << e.what()); + return nullptr; + } +} + +void AuthDBInstance::create_profile_from_schedule_mapping(const Tools::ScheduleMapping &mapping, + std::vector &profiles) { + auto profile = std::make_shared(); + auto schedule = mapping.schedule().load(); + + if (!schedule) { + return; + } + + add_doors_to_profile(mapping, profile, schedule); + + profiles.push_back(profile); +} + +void AuthDBInstance::add_doors_to_profile(const Tools::ScheduleMapping &mapping, + SimpleAccessProfilePtr &profile, + const Leosac::Tools::IScheduleCPtr& schedule) { + for (const auto &door : mapping.doors()) { + if (auto lazy_door = door.load()) { + std::string alias = lazy_door->alias(); + if (alias.empty()) { + continue; + } + + profile->addAccessSchedule(AuthTargetPtr(new AuthTarget(alias)), schedule); + + static const std::string ns_prefix = "my_leosac."; + if (alias.rfind(ns_prefix, 0) != 0) { + profile->addAccessSchedule(AuthTargetPtr(new AuthTarget(ns_prefix + alias)), schedule); + } + } + } + + if (mapping.doors().empty()) { + profile->addAccessSchedule(nullptr, schedule); + } +} + +bool AuthDBInstance::is_access_granted(IAccessProfilePtr &profile) { + auto now = std::chrono::system_clock::now(); + + if (target_name_.empty()) { + return profile->isAccessGranted(now, nullptr); + } else { + AuthTargetPtr target(new AuthTarget(target_name_)); + return profile->isAccessGranted(now, target); + } +} + +void AuthDBInstance::log_auth_event(const AuthResult &auth_result, Cred::ICredentialPtr &credentials) { + try { + if (!target_name_.empty()) { + using namespace odb; + using namespace odb::core; + auto db = core_utils_->database(); + odb::transaction t(db->begin()); + + auto audit = Audit::Factory::AuthEvent(db, credentials, target_name_); + audit->event_mask(auth_result.success ? Audit::EventType::AUTH_GRANTED : Audit::EventType::AUTH_DENIED); + audit->finalize(); + + t.commit(); + } else { + INFO("No target name provided, skipping Audit AuthEvent creation"); + } + } catch (const std::exception &e) { + WARN("Failed to create AuthEvent: " << e.what()); + } +} + +void AuthDBInstance::format_auth_result_msg(zmqpp::message &msg) { + msg << ("S_" + name_); +} + +std::string AuthDBInstance::format_user_name(const AuthResult &auth_result) { + using namespace Colorize; + if (auth_result.user) { + return Colorize::green(auth_result.user->username()); + } else { + return Colorize::red("UNKNOWN_USER"); + } +} + +void AuthDBInstance::update_and_log_auth_result_msg(const AuthResult &auth_result, zmqpp::message &auth_result_msg) { + using namespace Colorize; + std::string user = format_user_name(auth_result); + + if (auth_result.success) { + auth_result_msg << Leosac::Auth::AccessStatus::GRANTED; + INFO(Colorize::bold(name_) + << " " << Colorize::green("GRANTED") << " access to target " + << Colorize::underline(target_name_) << " for user " << user); + } else { + auth_result_msg << Leosac::Auth::AccessStatus::DENIED; + INFO(Colorize::bold(name_) + << " " << Colorize::red("DENIED") << " access to target " + << Colorize::underline(target_name_) << " for user " << user); + } +} + +void AuthDBInstance::log_credentials(Cred::ICredentialPtr &credentials) { + using namespace odb; + using namespace odb::core; + auto db = core_utils_->database(); + odb::transaction t(db->begin()); + + std::string cred_serialized; + cred_serialized = PolymorphicCredentialJSONStringSerializer::serialize( + *credentials, SystemSecurityContext::instance()); + INFO("Using Credentials: " << cred_serialized); + + t.commit(); +} + +bool AuthDBInstance::is_noise(const int nb_bits) const { + if (bits_low_threshold_ != -1 && nb_bits <= bits_low_threshold_) { + return true; + } + + if (bits_high_threshold_ != -1 && nb_bits >= bits_high_threshold_) { + return true; + } + + return false; +} \ No newline at end of file diff --git a/src/modules/auth/auth-db/AuthDBInstance.hpp b/src/modules/auth/auth-db/AuthDBInstance.hpp index 095439a3f..4008c71b6 100644 --- a/src/modules/auth/auth-db/AuthDBInstance.hpp +++ b/src/modules/auth/auth-db/AuthDBInstance.hpp @@ -19,44 +19,228 @@ #pragma once -#include "core/auth/AuthFwd.hpp" -#include "modules/auth/auth-db/AuthDBFwd.hpp" +#include "LeosacFwd.hpp" +#include "tools/db/DBService.hpp" +#include +#include +#include +#include +#include namespace Leosac { namespace Module { -namespace Auth +namespace Auth { -/** - * An instance of authentication handler that use the database - * to verify credentials and permissions. - */ -#pragma db object optimistic -class AuthDBInstance + +struct CredResult { - private: -#pragma db id auto - AuthDBInstanceId id_; + CredResult(bool i, Cred::ICredentialPtr c) + : ignore(i) + , db_credentials(c) + {} + + /** + * If true, the auth request should be ignored + * Used for kickback and other unwanted noise on the bus + */ + bool ignore; /** - * Target door. + * Credentials found in the database */ - ::Leosac::Auth::DoorLPtr door_; + Cred::ICredentialPtr db_credentials; +}; + +struct AuthResult +{ + AuthResult(bool s, bool i, ::Leosac::Auth::IAccessProfilePtr p, + ::Leosac::Auth::UserPtr u) + : success(s) + , ignore(i) + , profile(p) + , user(u) + {} -#pragma db version - size_t odb_version_; + /** + * Access granted or denied + */ + bool success; + /** + * If true, the auth request should be ignored + * Used for kickback and other unwanted noise on the bus + */ + bool ignore; - friend odb::access; + /** + * Profile used to grant or deny access. May be null if no profiles + * corresponding to the auth source were found. + */ + ::Leosac::Auth::IAccessProfilePtr profile; - // AUTH_SOURCE device. - // todo: Hardware service and related infrastructure improvements. + /** + * A user object representing the user who made the authentication + * attempt. + */ + ::Leosac::Auth::UserPtr user; +}; + +class AuthDBInstance : public std::enable_shared_from_this +{ + public: + /** + * Create an Authenticator instance that watches readers and writes authentication messages to the bus + * @param ctx the ZeroMQ context + * @param auth_ctx_name name of the instance in the auth_db module config + * @param auth_source_names names of the authentication sources for the instance in the auth_db module config + * @param auth_target_name name of the target attached to the instance + * @param core_utils core utilities + */ + AuthDBInstance(zmqpp::context &ctx, const std::string &auth_ctx_name, + const std::list &auth_sources_names, + const std::string &auth_target_name, + CoreUtilsPtr core_utils, + const int bits_low_threshold, + const int bits_high_threshold); + + ~AuthDBInstance(); + + AuthDBInstance(const AuthDBInstance &) = delete; + AuthDBInstance &operator=(const AuthDBInstance &) = delete; + + /** + * Handle a message from the bus + */ + void handle_bus_msg(); + + /** + * Get the bus subscription socket + */ + zmqpp::socket& bus_sub() { return bus_sub_; } + + private: + /** + * Handle a message from the kernel + */ + bool handle_kernel_msg(zmqpp::message &msg); + + /** + * Handle a message from an auth source + * Check if access is granted or denied by checking source against the database + */ + AuthResult handle_auth(zmqpp::message *msg) noexcept; + + /** + * Fetch credentials passed in message from database if they exist + */ + CredResult get_db_credentials(zmqpp::message *msg); + + /** + * Find credentials in database by card id and number of bits + */ + CredResult find_credentials_by_card_id(const std::string &card_id, const int nb_bits) const; + + /** + * Get the user from the db associated with the credentials + */ + ::Leosac::Auth::UserPtr get_user(Cred::ICredentialPtr &credentials); + + /** + * Build access profile for the user + */ + ::Leosac::Auth::IAccessProfilePtr build_profile(::Leosac::Auth::UserPtr &user, + Cred::ICredentialPtr &credentials); + + /** + * Checks if the profile has access to the target + */ + bool is_access_granted(::Leosac::Auth::IAccessProfilePtr &profile); + + /** + * Log auth event + */ + void log_auth_event(const AuthResult &auth_result, Cred::ICredentialPtr &credentials); + + /** + * Format auth result message + */ + void format_auth_result_msg(zmqpp::message &msg); + + /** + * Format user name for logging + */ + std::string format_user_name(const AuthResult &auth_result); + + /** + * Append access result to auth result and log event to console + */ + void update_and_log_auth_result_msg(const AuthResult &auth_result, zmqpp::message &auth_result_msg); + + /** + * Log credentials to console + */ + void log_credentials(Cred::ICredentialPtr &credentials); + + /** + * Create a profile from a schedule mapping and add it to the list of profiles + */ + void create_profile_from_schedule_mapping(const Tools::ScheduleMapping &mapping, + std::vector<::Leosac::Auth::IAccessProfilePtr> &profiles); + + /** + * Add targets to the profile from a schedule mapping + */ + void add_doors_to_profile(const Tools::ScheduleMapping &mapping, + Leosac::Auth::SimpleAccessProfilePtr &profile, + const ::Leosac::Tools::IScheduleCPtr& schedule); + + /** + * Check if the number of bits received is considered noise + */ + bool is_noise(const int nb_bits) const; + + /** + * Database service to query + */ + std::shared_ptr db_service_; + + /** + * Socket to write authentication messages to the bus + */ + zmqpp::socket bus_push_; + + /** + * Socket to read authentication messages from the bus + */ + zmqpp::socket bus_sub_; + + /** + * Name of the auth context instance + */ + std::string name_; + + /** + * Name of the target to authenticate against + */ + std::string target_name_; + + + CoreUtilsPtr core_utils_; + + std::mutex mutex_; + + /** + * Ignore <= this number of bits from bus + */ + int bits_low_threshold_; + + /** + * Ignore >= this number of bits from bus + */ + int bits_high_threshold_; }; } } -} - -#ifdef ODB_COMPILER -#include "core/auth/Door.hpp" -#endif +} \ No newline at end of file diff --git a/src/modules/auth/auth-db/AuthDBModule.cpp b/src/modules/auth/auth-db/AuthDBModule.cpp index 8ce0ae8fd..4ba8b032e 100644 --- a/src/modules/auth/auth-db/AuthDBModule.cpp +++ b/src/modules/auth/auth-db/AuthDBModule.cpp @@ -20,57 +20,71 @@ #include "modules/auth/auth-db/AuthDBModule.hpp" #include "core/CoreUtils.hpp" #include "core/kernel.hpp" -#include +#include "modules/auth/auth-db/AuthDBInstance.hpp" +#include using namespace Leosac; using namespace Leosac::Module::Auth; -AuthDBModule::AuthDBModule(zmqpp::context &ctx, zmqpp::socket *pipe, - const boost::property_tree::ptree &cfg, - CoreUtilsPtr utils) +AuthDBModule::AuthDBModule(zmqpp::context& ctx, zmqpp::socket *pipe, + const boost::property_tree::ptree &cfg, CoreUtilsPtr utils) : AsioModule(ctx, pipe, cfg, utils) { process_config(); - /* for (auto authenticator : authenticators_) - { - reactor_.add(authenticator->bus_sub(), - std::bind(&AuthDBInstance::handle_bus_msg, authenticator)); - }*/ + for (auto authenticator : authenticators_) + { + reactor_.add(authenticator->bus_sub(), + std::bind(&AuthDBInstance::handle_bus_msg, authenticator)); + } } -AuthDBModule::~AuthDBModule() -{ -} +AuthDBModule::~AuthDBModule() {} -void AuthDBModule::process_config() -{ - setup_database(); -} +void AuthDBModule::on_service_event(const service_event::Event &event) {} -void AuthDBModule::on_service_event(const service_event::Event &) -{ -} +void AuthDBModule::process_config() { + boost::property_tree::ptree auth_db_cfg = config_.get_child("module_config"); + + bits_low_threshold_ = auth_db_cfg.get("bits_low_threshold", -1); + bits_high_threshold_ = auth_db_cfg.get("bits_high_threshold", 127); -void AuthDBModule::setup_database() -{ - using namespace odb; - using namespace odb::core; - auto db = utils_->database(); - schema_version v = db->schema_version("module_auth-db"); - schema_version cv(schema_catalog::current_version(*db, "module_auth-db")); - if (v == 0) - { - transaction t(db->begin()); - schema_catalog::create_schema(*db, "module_auth-db"); - t.commit(); - } - else if (v < cv) - { - INFO("AuthDB Module performing database migration. Going from version " - << v << " to version " << cv); - transaction t(db->begin()); - schema_catalog::migrate(*db, cv, "module_auth-db"); - t.commit(); + INFO("Configured bits_low_threshold: " << bits_low_threshold_); + INFO("Configured bits_high_threshold: " << bits_high_threshold_); + + for (const auto &instance_node : auth_db_cfg.get_child("instances")) { + boost::property_tree::ptree auth_instance_cfg = instance_node.second; + std::string auth_ctx_name = auth_instance_cfg.get_child("name").data(); + std::string auth_target_name = auth_instance_cfg.get("target", ""); + std::list auth_sources_names; + + for (const auto &instance_subnode : auth_instance_cfg) { + if (instance_subnode.first == "auth_source") { + auth_sources_names.push_back(instance_subnode.second.data()); + } + } + + if (!auth_target_name.empty()) { + auth_target_name = utils_->kernel().config_manager().instance_name() + '.' + auth_target_name; + } + + INFO("Config processed for AuthDB instance: " << auth_ctx_name); + INFO(" - Target: " << auth_target_name); + INFO(" - Sources (" << auth_sources_names.size() << "): "); + for (const auto& source : auth_sources_names) { + INFO(" * " << source); + } + + setup_authenticators(auth_ctx_name, auth_sources_names, auth_target_name); } } + +void AuthDBModule::setup_authenticators(const std::string &auth_ctx_name, + const std::list &auth_sources_names, + const std::string &auth_target_name) +{ + authenticators_.push_back(AuthDBInstancePtr( + new AuthDBInstance(ctx_, auth_ctx_name, auth_sources_names, + auth_target_name, utils_, bits_low_threshold_, + bits_high_threshold_))); +} \ No newline at end of file diff --git a/src/modules/auth/auth-db/AuthDBModule.hpp b/src/modules/auth/auth-db/AuthDBModule.hpp index 3d940293a..06202fb8c 100644 --- a/src/modules/auth/auth-db/AuthDBModule.hpp +++ b/src/modules/auth/auth-db/AuthDBModule.hpp @@ -19,10 +19,11 @@ #pragma once -#include "modules/AsioModule.hpp" -#include -#include -#include +#include "AuthDBFwd.hpp" +#include "AsioModule.hpp" +#include "core/CoreUtils.hpp" +#include +#include namespace Leosac { @@ -30,35 +31,51 @@ namespace Module { namespace Auth { -class AuthDBInstance; /** -* This implements a authentication module that uses Leosac database -* to validate access. -*/ + * Module that provides authentication using database-stored credentials + */ class AuthDBModule : public AsioModule { - public: - AuthDBModule(zmqpp::context &ctx, zmqpp::socket *pipe, - const boost::property_tree::ptree &cfg, CoreUtilsPtr utils); - - AuthDBModule(const AuthDBModule &) = delete; + public: + AuthDBModule(zmqpp::context& ctx, zmqpp::socket *pipe, + const boost::property_tree::ptree &cfg, CoreUtilsPtr utils); + + AuthDBModule(const AuthDBModule &) = delete; + ~AuthDBModule(); - ~AuthDBModule(); + + /** + * Ignore less than this number of bits from bus + */ + int bits_low_threshold_; - protected: - void on_service_event(const service_event::Event &event) override; + /** + * Ignore bits higher than this number of bits from bus + */ + int bits_high_threshold_; + + protected: + void on_service_event(const service_event::Event &event) override; - private: - void process_config(); + private: + /** + * Process the module configuration from the kernel + */ + void process_config(); - void setup_database(); + /** + * Setup authenticator instances + */ + void setup_authenticators(const std::string &auth_ctx_name, + const std::list &auth_sources_names, + const std::string &auth_target_name); - /** - * Authenticator instance. - */ - // std::vector authenticators_; + /** + * List of the created authenticator instances + */ + std::list authenticators_; }; } } -} +} \ No newline at end of file diff --git a/src/modules/auth/auth-db/CMakeLists.txt b/src/modules/auth/auth-db/CMakeLists.txt index 4a6b03e76..5a403e274 100644 --- a/src/modules/auth/auth-db/CMakeLists.txt +++ b/src/modules/auth/auth-db/CMakeLists.txt @@ -3,55 +3,48 @@ set(AUTH-DB_BIN auth-db) set(AUTH-DB_SRCS init.cpp AuthDBModule.cpp - ) - -# Database support -set(OdbCMake_ODB_HEADERS - ${CMAKE_CURRENT_SOURCE_DIR}/AuthDBInstance.hpp - ) - -set(LEOSAC_ODB_INCLUDE_DIRS - ${CMAKE_SOURCE_DIR}/deps/zmqpp/src/ - ${CMAKE_SOURCE_DIR}/src - ${CMAKE_SOURCE_DIR}/deps/flagset - ${CMAKE_SOURCE_DIR}/deps/date/include - ${ZEROMQ_INCLUDE_DIR} - ${CMAKE_SOURCE_DIR}/src/db - ) - -set(OdbCMake_SOURCES "" AuthDBInstance.cpp AuthDBInstance.hpp) -odb_compile(OdbCMake_SOURCES - FILES ${OdbCMake_ODB_HEADERS} - MULTI_DATABASE dynamic - DB pgsql sqlite - STANDARD "c++14" - GENERATE_QUERY GENERATE_SESSION GENERATE_SCHEMA - SCHEMA_FORMAT embedded - SCHEMA_NAME module_auth-db - PROFILE "boost/date-time" "boost/optional" "leosac-profile" - INCLUDE_PREFIX modules/auth/auth-db - INCLUDE ${LEOSAC_ODB_INCLUDE_DIRS} - DEFAULT_POINTER "std::shared_ptr" - PGSQL_SERVER_VERSION "9.1" - ODB_EPILOGUE "#include \"core/audit/AuditEntry.hpp\"" - OUTPUT_DIR ${CMAKE_BINARY_DIR}/odb_gen/modules/auth/auth-db + AuthDBInstance.cpp ) add_library(${AUTH-DB_BIN} SHARED ${AUTH-DB_SRCS} - ${OdbCMake_SOURCES} ) set_target_properties(${AUTH-DB_BIN} PROPERTIES COMPILE_FLAGS "${MODULE_COMPILE_FLAGS}" ) -target_include_directories(${AUTH-DB_BIN} PUBLIC +find_library(LEOSAC_DB_LIB leosac_db PATHS ${CMAKE_BINARY_DIR} NO_DEFAULT_PATH) +find_library(LEOSAC_LIB_LIB leosac_lib PATHS ${CMAKE_BINARY_DIR} NO_DEFAULT_PATH) + +if(LEOSAC_DB_LIB AND LEOSAC_LIB_LIB) + target_link_libraries(${AUTH-DB_BIN} + ${Boost_LIBRARIES} + ${LEOSAC_DB_LIB} + ${LEOSAC_LIB_LIB} + ) +else() + target_link_libraries(${AUTH-DB_BIN} + ${Boost_LIBRARIES} + leosac_db + leosac_lib + ) +endif() + +target_include_directories(${AUTH-DB_BIN} + PUBLIC ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/modules + ${CMAKE_SOURCE_DIR}/deps/zmqpp/src + ${CMAKE_SOURCE_DIR}/deps/flagset + ${CMAKE_SOURCE_DIR}/deps/date/include + ${CMAKE_BINARY_DIR}/odb_gen + ${CMAKE_BINARY_DIR} + ${ZEROMQ_INCLUDE_DIR} ${ODB_INCLUDE_DIRS} ${ODB_COMPILE_OUTPUT_DIR} ) -target_link_libraries(${AUTH-DB_BIN} websock-api) - -install(TARGETS ${AUTH-DB_BIN} DESTINATION ${LEOSAC_MODULE_INSTALL_DIR}) +install(TARGETS ${AUTH-DB_BIN} + LIBRARY DESTINATION lib/leosac/modules + ) \ No newline at end of file diff --git a/src/modules/auth/auth-db/auth-db.md b/src/modules/auth/auth-db/auth-db.md index 1032c5a6a..8aa13be70 100644 --- a/src/modules/auth/auth-db/auth-db.md +++ b/src/modules/auth/auth-db/auth-db.md @@ -17,4 +17,149 @@ grant or deny access. Granting or denying access means sending a message to the application. This module doesn't open a door or perform action on its own. -@note Obviously this module requires that Leosac run with a database enabled. +@note This module requires that Leosac run with a database enabled. + +@note This module is suitable for systems with a large number of users/cards/groups as all data is stored in the database. + +Configuration Options {#mod_auth_db_user_config} +=================================================== + +Options | Options | Options | Options | Description | Mandatory +-----------|-----------|-----------------|--------------|-------------------------------------------------------------------|----------- +module_config | | | | Module configuration section | YES +---> | bits_low_threshold | | | Minimum number of bits to accept (ignores shorter as noise) | NO (default: -1) +---> | bits_high_threshold | | | Maximum number of bits to accept (ignores longer as noise) | NO (default: 127) +---> | instances | | | List of each link between reader and door(s) for authentication | YES +---> | ---> | instance | | Name of one particular auth context instance. Links with doorman module. Each instance under `instances` requires `` | YES +---> | ---> | ---> | name | Name of one particular auth context instance. Links with doorman module | YES +---> | ---> | ---> | auth_source | Which device (auth source) we listen to. Can appear multiple times. | YES +---> | ---> | ---> | target | Name of the target (door) that we are authenticating against | NO + +Notes: + + If the `target` is not present, the module assumes the default target, and will ignore target-specific permissions. + + The `bits_low_threshold` and `bits_high_threshold` help filter out noise from the bus. Values ≤ low_threshold or ≥ high_threshold are ignored. This is to help with systems that have inductive load (most door strikes are). D0 and D1 wires are very sensitive to any type of noise. Note that from a hardware perspective, you should have a diode across your door strike if it's an inductive load, since the kickback could potentially damage the GPIO pins. + + You can enter multiple `auth_source` devices. The module instance will listen to all of them. + + The `target` field is prefixed by the instance name and a dot when checking for permission in the database. This should match the GPIO out name in the `LIBGPIOD` module. + +Noise Filtering {#mod_auth_db_noise_filtering} +============================================== + +The AuthDB module includes intelligent noise filtering to prevent false authentication attempts from electrical interference or corrupted data on the bus. + +### Bit Threshold Configuration + +- **`bits_low_threshold`**: Any credential with fewer bits than this value will be ignored as noise +- **`bits_high_threshold`**: Any credential with more bits than this value will be ignored as corruption + +### Default Values + +- **Low threshold**: -1 (disabled by default) +- **High threshold**: 127 (maximum reasonable bit length) + +### Example Configuration + +```xml + + 8 + 64 + + +``` + +This configuration would: +- Ignore any credentials with ≤8 bits (likely noise) +- Ignore any credentials with ≥64 bits (likely noise) +- Only process credentials with 9-63 bits + +If for example, you only wanted to monitor attempts with 27 bit wiegand data, you could set the following: + +```xml + + 26 + 28 + + +``` + +This configuration would: +- Ignore any attempts that aren't 27 bits +- Even if there are valid attempts made with cards that aren't 27 bits, it would ignore it. + +Users {#mod_auth_db_user} +====================== + +Users are stored in the Leosac database and are a core component of the authentication system. +Users map to groups, credentials, schedules, etc. + +Users can hold additional properties like their name, email address, or the status of their account (`enabled`, or `disabled`). + +Credentials {#mod_auth_db_cred} +================================ + +Credentials are stored in the database and map to users. The module supports multiple credential types: + +### Supported Credential Types + +1. **RFIDCard**: Standard RFID card credentials +2. **PinCode**: PIN code authentication (not yet implemented) +3. **RFIDCardPin**: Combination of RFID card and PIN code (not yet implemented) + +Audit Logging {#mod_auth_db_audit} +=================================== + +The module provides comprehensive audit logging for all authentication attempts: + +### Logged Events + +- **AUTH_GRANTED**: Successful authentication attempts +- **AUTH_DENIED**: Failed authentication attempts +- **Credential usage**: Details about which credentials were used +- **Target information**: Which door/area was accessed +- **User information**: Who attempted the access + +### Audit Data + +Each audit event includes: +- Timestamp of the attempt +- Credential used +- Target accessed +- User attempting access +- Result (granted/denied) +- Access profile used + +Example Configuration {#mod_auth_db_example} +================================ + +This is the module configuration in the main config file: + +```xml + + AUTH-MANAGER-DB + libauth-db.so + 41 + + 8 + 64 + + + AUTH_CONTEXT_1 + MY_WIEGAND_1 + MY_WIEGAND_2 + MAIN_ENTRANCE + + + AUTH_CONTEXT_2 + BACK_DOOR_READER + BACK_ENTRANCE + + + + +``` + +This configuration: +- Sets noise filtering thresholds (8-64 bits) +- Creates two authentication contexts +- Each context listens to different readers +- Each context targets different doors + diff --git a/src/modules/doorman/CMakeLists.txt b/src/modules/doorman/CMakeLists.txt index 828da04a0..59205aa24 100644 --- a/src/modules/doorman/CMakeLists.txt +++ b/src/modules/doorman/CMakeLists.txt @@ -13,4 +13,34 @@ set_target_properties(${DOORMAN_BIN} PROPERTIES COMPILE_FLAGS "${MODULE_COMPILE_FLAGS}" ) +find_library(LEOSAC_DB_LIB leosac_db PATHS ${CMAKE_BINARY_DIR} NO_DEFAULT_PATH) +find_library(LEOSAC_LIB_LIB leosac_lib PATHS ${CMAKE_BINARY_DIR} NO_DEFAULT_PATH) + +if(LEOSAC_DB_LIB AND LEOSAC_LIB_LIB) + target_link_libraries(${DOORMAN_BIN} + ${LEOSAC_DB_LIB} + ${LEOSAC_LIB_LIB} + ) +else() + target_link_libraries(${DOORMAN_BIN} + leosac_db + leosac_lib + ) +endif() + +target_include_directories(${DOORMAN_BIN} + PUBLIC + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/modules + ${CMAKE_SOURCE_DIR}/deps/zmqpp/src + ${CMAKE_SOURCE_DIR}/deps/flagset + ${CMAKE_SOURCE_DIR}/deps/date/include + ${CMAKE_BINARY_DIR}/odb_gen + ${CMAKE_BINARY_DIR} + ${ZEROMQ_INCLUDE_DIR} + ${ODB_INCLUDE_DIRS} + ${ODB_COMPILE_OUTPUT_DIR} + ) + + install(TARGETS ${DOORMAN_BIN} DESTINATION ${LEOSAC_MODULE_INSTALL_DIR}) diff --git a/src/modules/doorman/DoormanDoor.cpp b/src/modules/doorman/DoormanDoor.cpp index 919b233e0..349a7bae1 100644 --- a/src/modules/doorman/DoormanDoor.cpp +++ b/src/modules/doorman/DoormanDoor.cpp @@ -92,3 +92,14 @@ std::chrono::system_clock::time_point DoormanDoor::contact_lastupdate() const { return contact_lastupdate_; } + +bool DoormanDoor::is_door_override_active() const +{ + auto now = std::chrono::system_clock::now(); + return now < door_override_until_; +} + +void DoormanDoor::set_door_override_until(std::chrono::system_clock::time_point until) +{ + door_override_until_ = until; +} \ No newline at end of file diff --git a/src/modules/doorman/DoormanDoor.hpp b/src/modules/doorman/DoormanDoor.hpp index 3e14d6668..e778d1fa8 100644 --- a/src/modules/doorman/DoormanDoor.hpp +++ b/src/modules/doorman/DoormanDoor.hpp @@ -67,6 +67,9 @@ class DoormanDoor void alarm_forced(const std::string& alarm); + bool is_door_override_active() const; + void set_door_override_until(std::chrono::system_clock::time_point until); + private: Leosac::Auth::AuthTargetPtr door_; @@ -82,6 +85,8 @@ class DoormanDoor std::chrono::system_clock::time_point contact_lastupdate_; std::string alarm_forced_; + + std::chrono::system_clock::time_point door_override_until_; }; } } diff --git a/src/modules/doorman/DoormanInstance.cpp b/src/modules/doorman/DoormanInstance.cpp index 1768598a8..2dc3b0cd1 100644 --- a/src/modules/doorman/DoormanInstance.cpp +++ b/src/modules/doorman/DoormanInstance.cpp @@ -53,8 +53,17 @@ DoormanInstance::DoormanInstance(DoormanModule &module, zmqpp::context &ctx, for (auto &d : module.doors()) { - auto door = std::make_shared(d, ctx); - doors_.push_back(door); + bool door_in_instance = false; + for (const auto &action : actions_) { + if (action.target_ == d->gpio()->name()) { + door_in_instance = true; + break; + } + } + if (door_in_instance) { + auto door = std::make_shared(d, ctx); + doors_.push_back(door); + } } } @@ -81,6 +90,8 @@ void DoormanInstance::handle_bus_msg() DEBUG("ACTION (target = " << action.target_ << ")"); zmqpp::message msg; + int door_duration = 0; + for (auto &frame : action.cmd_) { // we try to convert argument to int. if it works we send as int64_t, @@ -90,6 +101,9 @@ void DoormanInstance::handle_bus_msg() try { v = std::stoi(frame); + if (msg.parts() == 1 && v > 0) { + door_duration = v; + } } catch (...) { @@ -101,6 +115,17 @@ void DoormanInstance::handle_bus_msg() msg << static_cast(v); DEBUG("would do : " << frame << " to target: " << action.target_); } + + if (door_duration > 0) { + for (auto& door : doors()) { + if (door->door()->gpio()->name() == action.target_) { + auto override_end = std::chrono::system_clock::now() + std::chrono::milliseconds(door_duration); + door->set_door_override_until(override_end); + break; + } + } + } + command_send_recv(action.target_, std::move(msg)); } } @@ -141,8 +166,7 @@ bool DoormanInstance::ignore_action(const DoormanAction &action, return true; auto target = find_target(action.target_); - if (target && (target->is_always_closed(std::chrono::system_clock::now()) || - target->is_always_open(std::chrono::system_clock::now()))) + if (target && target->is_always_open(std::chrono::system_clock::now())) { INFO("Door " << target->name() << " is in immutable state (always open, " "or always closed) so we ignore this " diff --git a/src/modules/doorman/DoormanInstance.hpp b/src/modules/doorman/DoormanInstance.hpp index 597d84d21..2ef36735b 100644 --- a/src/modules/doorman/DoormanInstance.hpp +++ b/src/modules/doorman/DoormanInstance.hpp @@ -98,7 +98,7 @@ class DoormanInstance * There are multiple reason why we might wanna ignore an action: * 1. The expected status (`granted` / `denied`) does not match the received * status. - * 2. The door is in always_open (or alway_closed) mode. + * 2. The door is in always_open mode. */ bool ignore_action(const DoormanAction &action, Auth::AccessStatus status) const; diff --git a/src/modules/doorman/DoormanModule.cpp b/src/modules/doorman/DoormanModule.cpp index 3aef8b6d1..39705f361 100644 --- a/src/modules/doorman/DoormanModule.cpp +++ b/src/modules/doorman/DoormanModule.cpp @@ -24,14 +24,26 @@ #include "core/kernel.hpp" #include "hardware/facades/FAlarm.hpp" #include "tools/log.hpp" +#include "tools/service/ServiceRegistry.hpp" +#include "tools/db/DBService.hpp" +#include "tools/Schedule.hpp" +#include "tools/ScheduleMapping.hpp" +#include "core/auth/Door.hpp" +#include "core/auth/Door_odb.h" +#include using namespace Leosac::Module::Doorman; using namespace Leosac::Auth; +const std::chrono::seconds DoormanModule::SCHEDULE_REFRESH_INTERVAL; + DoormanModule::DoormanModule(zmqpp::context &ctx, zmqpp::socket *pipe, const boost::property_tree::ptree &cfg, CoreUtilsPtr utils) : BaseModule(ctx, pipe, cfg, utils) + , use_db_schedules_(false) + , db_service_(nullptr) + { try { @@ -60,9 +72,14 @@ void DoormanModule::process_config() { boost::property_tree::ptree module_config = config_.get_child("module_config"); + use_db_schedules_ = module_config.get("use_db_schedules", false); + set_db_service(); + auto doors_cfg = module_config.get_child_optional("doors"); if (doors_cfg) process_doors_config(*doors_cfg); + if (use_db_schedules_) + process_db_schedules(); for (const auto &node : module_config.get_child("instances")) { @@ -110,6 +127,18 @@ void DoormanModule::process_config() } } +void DoormanModule::set_db_service() { + if (use_db_schedules_) { + db_service_ = utils_->service_registry().get_service(); + if (!db_service_) { + WARN("Database schedules requested but DBService not available. Falling back to config schedules."); + use_db_schedules_ = false; + } else { + INFO("Using database schedules for doorman module"); + } + } +} + void DoormanModule::run() { while (is_running_) @@ -136,7 +165,7 @@ void DoormanModule::process_doors_config( door->gpio( std::unique_ptr(new Hardware::FGPIO(ctx_, gpio))); - if (open_schedule) + if (!use_db_schedules_ && open_schedule) { Tools::XmlScheduleLoader xml_sched; xml_sched.load(*open_schedule); @@ -184,9 +213,238 @@ void DoormanModule::process_doors_config( } } +void DoormanModule::process_db_schedules() { + try { + auto db = db_service_->db(); + odb::transaction t(db->begin()); + odb::result schedules = db->query(); + std::map> door_open_timeframes; + + clear_door_schedules(); + + add_open_door_schedules(schedules, door_open_timeframes); + add_close_door_schedules(door_open_timeframes); + + t.commit(); + } catch (const std::exception &e) { + ERROR("Failed to process database schedules: " << e.what()); + use_db_schedules_ = false; + } +} + +void DoormanModule::add_open_door_schedules(odb::result &schedules, + std::map> &door_open_timeframes) +{ + for (const auto &schedule : schedules) { + for (const auto &mapping : schedule.mapping()) { + if (is_door_schedule(mapping)) { + for (const auto &lazy_door : mapping->doors()) { + auto door_ptr = lazy_door.load(); + if (door_ptr) { + std::string door_name = door_ptr->alias(); + for (auto &door : doors_) { + if (door->name() == door_name) { + auto schedule_copy = std::make_shared(schedule); + door->add_always_open_sched(schedule_copy); + + for (const auto &tf : schedule_copy->timeframes()) { + door_open_timeframes[door_name].push_back(tf); + } + } + } + } + } + } + // TODO: Add schedules to doors in zones + } + } +} + +void DoormanModule::add_close_door_schedules(std::map> &door_open_timeframes) +{ + for (const auto &door : doors_) { + std::string door_name = door->name(); + auto timeframes = door_open_timeframes[door_name]; + if (auto closed_schedule = create_closed_schedule(timeframes)) { + log_open_and_closed_timeframes(timeframes, closed_schedule, door_name); + door->add_always_close_sched(closed_schedule); + } else { + INFO("Door " << door_name << " is always open"); + } + } +} + +std::shared_ptr DoormanModule::create_closed_schedule(const std::vector &open_timeframes) { + if (open_timeframes.empty()) return create_24_7_schedule(); + + auto closed_schedule = std::make_shared("closed_schedule"); + + std::map> open_timeframes_grouped_by_day; + for (const auto &tf : open_timeframes) { + open_timeframes_grouped_by_day[tf.day].push_back(tf); + } + + for (int day = 0; day < 7; ++day) { + auto open_tf_map = open_timeframes_grouped_by_day.find(day); + if (open_tf_map == open_timeframes_grouped_by_day.end() || open_tf_map->second.empty()) { + add_timeframe_to_schedule(closed_schedule, day, 0, 0, 23, 59); + continue; + } + + auto &open_tfs = open_tf_map->second; + sort_tf_vec_by_time(open_tfs); + add_closed_tfs(open_tfs, closed_schedule, day); + } + + return closed_schedule; +} + +std::shared_ptr DoormanModule::create_24_7_schedule() { + auto schedule = std::make_shared("closed_schedule"); + for (int day = 0; day < 7; ++day) { + add_timeframe_to_schedule(schedule, day, 0, 0, 23, 59); + } + return schedule; +} + +void DoormanModule::add_timeframe_to_schedule(std::shared_ptr schedule, int day, int start_hour, int start_min, int end_hour, int end_min) { + Tools::SingleTimeFrame tf(day, start_hour, start_min, end_hour, end_min); + schedule->add_timeframe(tf); +} + +void DoormanModule::sort_tf_vec_by_time(std::vector &timeframes) { + std::sort(timeframes.begin(), timeframes.end(), + [](const Tools::SingleTimeFrame &a, const Tools::SingleTimeFrame &b) { + if (a.start_hour != b.start_hour) { + return a.start_hour < b.start_hour; + } + return a.start_min < b.start_min; + }); +} + +void DoormanModule::add_closed_tfs(std::vector &open_tfs, std::shared_ptr closed_schedule, int day) { + auto add_tf = [&](int start_hour, int start_min, int end_hour, int end_min) { + if (start_hour < end_hour || (start_hour == end_hour && start_min < end_min)) { + adjust_tf_times(start_hour, start_min, end_hour, end_min); + add_timeframe_to_schedule(closed_schedule, day, start_hour, start_min, end_hour, end_min); + } + }; + + auto &first_tf = open_tfs[0]; + add_tf(0, 0, first_tf.start_hour, first_tf.start_min); + + int max_tf_index = open_tfs.size() - 1; + for (int i = 0; i < max_tf_index; ++i) { + const auto ¤t_tf = open_tfs[i]; + const auto &next_tf = open_tfs[i + 1]; + add_tf(current_tf.end_hour, current_tf.end_min, next_tf.start_hour, next_tf.start_min); + } + + auto &last_tf = open_tfs.back(); + add_tf(last_tf.end_hour, last_tf.end_min, 23, 59); +} + +void DoormanModule::adjust_tf_times(int &start_hour, int &start_min, int &end_hour, int &end_min) { + if ((start_hour == 0 && start_min == 0) || (end_hour == 0 && end_min == 0)) {} + else { + if(start_min == 59) { + start_hour++; + start_min = 0; + } else { + start_min++; + } + } + + if (end_min == 0) { + end_min = 59; + end_hour--; + } else if (end_hour != 23) { + end_min--; + } +} + +void DoormanModule::log_open_and_closed_timeframes(const std::vector &open_tfs, + const std::shared_ptr &closed_schedule, + const std::string &door_name) +{ + INFO("=== SCHEDULE SUMMARY FOR DOOR: " << door_name << " ==="); + + INFO("OPEN timeframes (" << open_tfs.size() << " total):"); + for (size_t i = 0; i < open_tfs.size(); ++i) { + const auto& tf = open_tfs[i]; + std::string day_name; + switch(tf.day) { + case 0: day_name = "Sunday"; break; + case 1: day_name = "Monday"; break; + case 2: day_name = "Tuesday"; break; + case 3: day_name = "Wednesday"; break; + case 4: day_name = "Thursday"; break; + case 5: day_name = "Friday"; break; + case 6: day_name = "Saturday"; break; + default: day_name = "Unknown"; break; + } + + INFO(" Open " << (i + 1) << ": " << day_name << " " + << std::setfill('0') << std::setw(2) << tf.start_hour << ":" + << std::setfill('0') << std::setw(2) << tf.start_min << " to " + << std::setfill('0') << std::setw(2) << tf.end_hour << ":" + << std::setfill('0') << std::setw(2) << tf.end_min); + } + + INFO("CLOSED timeframes (" << closed_schedule->timeframes().size() << " total):"); + auto created_timeframes = closed_schedule->timeframes(); + for (size_t i = 0; i < created_timeframes.size(); ++i) { + const auto& tf = created_timeframes[i]; + std::string day_name; + switch(tf.day) { + case 0: day_name = "Sunday"; break; + case 1: day_name = "Monday"; break; + case 2: day_name = "Tuesday"; break; + case 3: day_name = "Wednesday"; break; + case 4: day_name = "Thursday"; break; + case 5: day_name = "Friday"; break; + case 6: day_name = "Saturday"; break; + default: day_name = "Unknown"; break; + } + + INFO(" Closed " << (i + 1) << ": " << day_name << " " + << std::setfill('0') << std::setw(2) << tf.start_hour << ":" + << std::setfill('0') << std::setw(2) << tf.start_min << " to " + << std::setfill('0') << std::setw(2) << tf.end_hour << ":" + << std::setfill('0') << std::setw(2) << tf.end_min); + } + + INFO("=== END SCHEDULE SUMMARY ==="); +} + +void DoormanModule::clear_door_schedules() { + for (auto &door : doors_) + door->clear_schedules(); +} + +bool DoormanModule::is_door_schedule(const Tools::ScheduleMappingPtr &mapping) { + bool has_doors = !mapping->doors().empty(); + bool has_users = !mapping->users().empty(); + bool has_groups = !mapping->groups().empty(); + bool has_credentials = !mapping->credentials().empty(); + bool has_zones = !mapping->zones().empty(); + + return (has_doors || has_zones) && !has_users && !has_groups && !has_credentials; +} + +void DoormanModule::refresh_db_schedules(std::chrono::system_clock::time_point now) { + if (!use_db_schedules_ || !db_service_) return; + + if ((now - last_schedule_refresh_) >= SCHEDULE_REFRESH_INTERVAL) { + process_db_schedules(); + last_schedule_refresh_ = now; + } +} + void DoormanModule::update() { auto now = std::chrono::system_clock::now(); + refresh_db_schedules(now); for (auto &&doorman : doormen_) { @@ -217,7 +475,10 @@ void DoormanModule::update() door->alarm_forced(""); } } - d->resetToExpectedState(now); + + if (!door->is_door_override_active()) { + d->resetToExpectedState(now); + } } } } diff --git a/src/modules/doorman/DoormanModule.hpp b/src/modules/doorman/DoormanModule.hpp index 3fdec4329..8ed69dab9 100644 --- a/src/modules/doorman/DoormanModule.hpp +++ b/src/modules/doorman/DoormanModule.hpp @@ -23,9 +23,9 @@ #include "hardware/facades/FGPIO.hpp" #include "modules/BaseModule.hpp" #include "tools/XmlScheduleLoader.hpp" +#include "tools/db/db_fwd.hpp" +#include "tools/Schedule.hpp" #include -#include -#include #include namespace Leosac @@ -66,8 +66,6 @@ class DoormanModule : public BaseModule const std::vector &doors() const; private: - void update(); - /** * Processing the configuration tree, spawning AuthFileInstance object as * described in the @@ -77,6 +75,83 @@ class DoormanModule : public BaseModule void process_doors_config(const boost::property_tree::ptree &t); + void process_db_schedules(); + + /** + * Adds open schedules to the doors from the database. + */ + void add_open_door_schedules(odb::result &schedules, std::map> &door_open_timeframes); + + /** + * Adds close schedules to the doors from the database. + */ + void add_close_door_schedules(std::map> &door_open_timeframes); + + /** + * Creates an inverse schedule based off of the open timeframes to close the door + * when the open schedule is not active. + */ + std::shared_ptr create_closed_schedule(const std::vector &open_timeframes); + + /** + * Creates a schedule with 7 timeframes (all days 00:00-23:59). + */ + std::shared_ptr create_24_7_schedule(); + + /** + * Adds the beginning of the day timeframe to a schedule. + * Ex: If the open schedule is 08:00-17:00, the closed schedule will be 00:00-08:00. + */ + void add_closed_tfs(std::vector &open_tfs, std::shared_ptr closed_schedule, int day); + + /** + * Logs the open and closed timeframes for a door. + */ + void log_open_and_closed_timeframes(const std::vector &open_tfs, + const std::shared_ptr &closed_schedule, + const std::string &door_name); + + /** + * Adds a timeframe to a schedule. + */ + void add_timeframe_to_schedule(std::shared_ptr schedule, int day, int start_hour, int start_min, int end_hour, int end_min); + + /** + * Sorts a vector of timeframes by start time hh:mm. + * Does not sort by day. + */ + void sort_tf_vec_by_time(std::vector &timeframes); + + /** + * Adds and subtracts 1 minute from the start and end times of a timeframe. + * Prevents open and close start/end times from being the same. + * Ex: Open: 14:00-18:30 -> Closed: 14:01-18:29 + */ + void adjust_tf_times(int &start_hour, int &start_min, int &end_hour, int &end_min); + + /** + * Sets the db_service_ member variable if use_db_schedules_ is true. + */ + void set_db_service(); + + /** + * Clears always on and off schedules from all doors. + */ + void clear_door_schedules(); + + /** + * Re-checks the database for updated/deleted schedules + * Removes all schedules and re-adds them. + */ + void refresh_db_schedules(std::chrono::system_clock::time_point now); + + /** + * Checks if a schedule mapping contains only doors + */ + bool is_door_schedule(const Tools::ScheduleMappingPtr &mapping); + + void update(); + /** * Authenticator instances. */ @@ -86,6 +161,14 @@ class DoormanModule : public BaseModule * Doors, to manage the always-on or always off stuff. */ std::vector doors_; + + bool use_db_schedules_; + + DBServicePtr db_service_; + + std::chrono::system_clock::time_point last_schedule_refresh_; + + static constexpr std::chrono::seconds SCHEDULE_REFRESH_INTERVAL{60}; }; } } diff --git a/src/modules/doorman/doorman.md b/src/modules/doorman/doorman.md index 02b5f7895..7d42318b1 100644 --- a/src/modules/doorman/doorman.md +++ b/src/modules/doorman/doorman.md @@ -26,6 +26,7 @@ Configuration Options {#mod_doorman_user_config} Options | Options | Options | Options | Options | Description | Mandatory -----------|-----------|-----------------|--------------|-------------|-------------------------------------------------------------------|----------- +use_db_schedules | | | | | When set to true, use database schedules instead of config schedules | NO (default: false) instances | | | | | Multiples (unrelated) doorman instance are allowed | YES ---> | instance | | | | Configuration information for 1 instance | YES ---> | ---> | name | | | Name of this doorman instance | YES diff --git a/src/modules/libgpiod/LibgpiodPin.cpp b/src/modules/libgpiod/LibgpiodPin.cpp index dd84cbc32..e3c436542 100644 --- a/src/modules/libgpiod/LibgpiodPin.cpp +++ b/src/modules/libgpiod/LibgpiodPin.cpp @@ -111,6 +111,11 @@ void LibgpiodPin::handle_message() ok = turn_off(); else if (frame1 == "TOGGLE") ok = toggle(); + else if (frame1 == "STATE") { + bool state = read_value(); + sock_.send(state ? "ON" : "OFF"); + return; + } sock_.send(ok ? "OK" : "KO"); // publish new state. diff --git a/src/modules/websock-api/WSServer.cpp b/src/modules/websock-api/WSServer.cpp index 10d29c0de..3720e61de 100644 --- a/src/modules/websock-api/WSServer.cpp +++ b/src/modules/websock-api/WSServer.cpp @@ -75,10 +75,11 @@ using namespace Leosac::Module::WebSockAPI; using json = nlohmann::json; -WSServer::WSServer(WebSockAPIModule &module, DBPtr database) +WSServer::WSServer(WebSockAPIModule &module, DBPtr database, bool enable_logging) : auth_(*this) , dbsrv_(std::make_shared(database)) , module_(module) + , enable_logging_(enable_logging) { ASSERT_LOG(database, "No database object passed into WSServer."); using websocketpp::lib::placeholders::_1; @@ -89,9 +90,11 @@ WSServer::WSServer(WebSockAPIModule &module, DBPtr database) srv_.set_close_handler(std::bind(&WSServer::on_close, this, _1)); srv_.set_message_handler(std::bind(&WSServer::on_message, this, _1, _2)); srv_.set_reuse_addr(true); - // clear all logs. - // srv_.clear_access_channels(websocketpp::log::alevel::all); - + + if (!enable_logging_) { + srv_.clear_access_channels(websocketpp::log::alevel::all); + srv_.clear_error_channels(websocketpp::log::elevel::all); + } // Register internal handlers, ie handler that are managed by the Websocket // module itself. diff --git a/src/modules/websock-api/WSServer.hpp b/src/modules/websock-api/WSServer.hpp index cc714769b..ea7089db2 100644 --- a/src/modules/websock-api/WSServer.hpp +++ b/src/modules/websock-api/WSServer.hpp @@ -65,7 +65,7 @@ class WSServer * @param database A (non-null) pointer to the * database. */ - WSServer(WebSockAPIModule &module, DBPtr database); + WSServer(WebSockAPIModule &module, DBPtr database, bool enable_logging = true); ~WSServer(); using Server = websocketpp::server; @@ -257,6 +257,11 @@ class WSServer */ WebSockAPIModule &module_; + /** + * WebSocket logging configuration + */ + bool enable_logging_; + /** * Work used to keep the io_service alive while someone * has a reference to (WS) Service object. diff --git a/src/modules/websock-api/WebSockAPI.cpp b/src/modules/websock-api/WebSockAPI.cpp index 3c4633216..18b204fa9 100644 --- a/src/modules/websock-api/WebSockAPI.cpp +++ b/src/modules/websock-api/WebSockAPI.cpp @@ -36,6 +36,7 @@ WebSockAPIModule::WebSockAPIModule(zmqpp::context &ctx, zmqpp::socket *pipe, { port_ = cfg.get("module_config.port", 8976); interface_ = cfg.get("module_config.interface", "127.0.0.1"); + enable_websocket_logging_ = cfg.get("module_config.enable_logging", true); auto endpoint_colorized = Colorize::green( Colorize::underline(fmt::format("{}:{}", interface_, port_))); @@ -45,7 +46,7 @@ WebSockAPIModule::WebSockAPIModule(zmqpp::context &ctx, zmqpp::socket *pipe, void WebSockAPIModule::run() { - wssrv_ = std::make_unique(*this, core_utils()->database()); + wssrv_ = std::make_unique(*this, core_utils()->database(), enable_websocket_logging_); std::thread thread(std::bind(&WSServer::run, wssrv_.get(), interface_, port_)); while (is_running_) diff --git a/src/modules/websock-api/WebSockAPI.hpp b/src/modules/websock-api/WebSockAPI.hpp index 16c6640db..342d8c8ad 100644 --- a/src/modules/websock-api/WebSockAPI.hpp +++ b/src/modules/websock-api/WebSockAPI.hpp @@ -70,6 +70,11 @@ class WebSockAPIModule : public BaseModule */ std::string interface_; + /** + * WebSocket logging configuration + */ + bool enable_websocket_logging_; + /** * Our websocket server object. */