Files
logos-capability-module/tests/test_capability_module.cpp
T
Iuri Matias 22e54ffc79 refactor capability module to work as an internal module proper instead of cosplaying as a app layer module
refactor capability module to work as an internal module proper instead of cosplaying as a app layer module

refactor capability module to work as an internal module proper instead of cosplaying as a app layer module

update flake

update flake
2026-06-11 16:31:00 -04:00

313 lines
11 KiB
C++

// Unit tests for CapabilityModulePlugin.
//
// capability_module is a plain legacy Qt plugin: the generic ModuleProxy wraps
// it and dispatches Q_INVOKABLE methods by name, so the tests drive the plugin
// methods directly (initLogos + requestModule / registerRestriction).
//
// requestModule() mints a UUID auth token, asks the target module to record it
// via informModuleToken_module(), and returns the token to the caller.
//
// Security contract (F-001, CWE-290): requestModule fails closed. It refuses to
// mint a token unless BOTH the requesting identity (fromModuleName) and the
// target (moduleName) are modules capability_module already knows about — i.e.
// have a token registered in the TokenManager (the host seeds one entry per
// loaded module via notifyCapabilityModule). An empty, unknown, or never-loaded
// name yields an empty result and no token is minted. This is defense-in-depth:
// it blocks spoofing a *non-loaded* identity, but cannot by itself stop a loaded
// module from presenting *another loaded module's* name — that needs the RPC
// layer to surface the verified caller token to this method.
//
// In mock mode, MockLogosObject::informModuleToken always returns true and
// records nothing, so the success-path tests focus on the externally-observable
// contract: the returned token's shape and uniqueness. Verifying the dispatch to
// informModuleToken_module would require extending the mock framework to record
// those calls.
#include <logos_test.h>
#include <logos_mock.h>
#include <QRegularExpression>
#include <QSet>
#include <QString>
#include "capability_module_plugin.h"
#include "logos_api.h"
#include "token_manager.h"
namespace {
// UUID without braces: 8-4-4-4-12 lowercase hex digits separated by hyphens.
const QRegularExpression kUuidRegex(
QStringLiteral("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"));
// Seed a module's token so capability_module treats it as a known/loaded module.
void seedModule(const QString& name) {
TokenManager::instance().saveToken(name, "seed-token-" + name);
}
// The trusted core/capability_module auth token. registerRestriction requires
// it; only core holds it in production. In tests it is whatever seedModule
// stored for "capability_module".
const QString kTrustedToken = QStringLiteral("seed-token-capability_module");
// Seed the trusted channel so registerRestriction calls authenticate. Call in
// any test that registers a restriction.
void seedTrustedChannel() {
seedModule("capability_module");
}
}
// ── Success path: both caller and target are known modules ──────────────────
LOGOS_TEST(requestModule_returns_uuid_format_token) {
LogosMockSetup mock;
seedModule("requester_module");
seedModule("target_module");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
}
LOGOS_TEST(requestModule_mints_unique_token_per_call) {
LogosMockSetup mock;
seedModule("requester");
seedModule("target");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
QSet<QString> tokens;
for (int i = 0; i < 10; ++i) {
tokens.insert(plugin.requestModule("requester", "target"));
}
LOGOS_ASSERT_EQ(tokens.size(), 10);
}
LOGOS_TEST(requestModule_works_when_target_token_is_pre_seeded) {
LogosMockSetup mock;
// Seed both the caller and the target — exercises the getToken() path for
// the target while satisfying the known-caller gate.
seedModule("requester_module");
TokenManager::instance().saveToken("target_module", "pre-seeded-token");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
}
// ── F-001 security regression: fail closed on unverified input ──────────────
LOGOS_TEST(requestModule_returns_empty_when_not_initialized) {
LogosMockSetup mock;
CapabilityModulePlugin plugin;
QString token = plugin.requestModule("requester", "target");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
LOGOS_TEST(requestModule_rejects_empty_fromModuleName) {
LogosMockSetup mock;
seedModule("target_module");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
QString token = plugin.requestModule("", "target_module");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
LOGOS_TEST(requestModule_rejects_unknown_fromModuleName) {
LogosMockSetup mock;
// Only the target is known; the requesting identity was never loaded.
seedModule("target_module");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
// Spoofing a non-loaded identity must not mint a token.
QString token = plugin.requestModule("spoofed_module", "target_module");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
LOGOS_TEST(requestModule_rejects_unknown_target) {
LogosMockSetup mock;
// Only the caller is known; the target was never loaded.
seedModule("requester_module");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
QString token = plugin.requestModule("requester_module", "missing_target");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
LOGOS_TEST(requestModule_succeeds_for_known_caller_and_target) {
LogosMockSetup mock;
seedModule("requester_module");
seedModule("target_module");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
}
// ── Access-policy enforcement (registerRestriction + requestModule) ─────────
//
// Core parses the access policy and calls registerRestriction(target,
// allowedCallers) for each restricted target. requestModule then refuses to
// mint a token when a restricted target's allowed-caller set does not include
// the requester — the denied caller never gets credentials, so it can never
// call the target. A target with NO registered restriction stays unrestricted.
LOGOS_TEST(registerRestriction_rejects_empty_target) {
LogosMockSetup mock;
seedTrustedChannel();
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
LOGOS_ASSERT_FALSE(plugin.registerRestriction(kTrustedToken, "", QStringList{"caller"}));
}
LOGOS_TEST(registerRestriction_rejects_untrusted_caller_token) {
// A loaded module can reach this method (isAuthorized accepts any issued
// token), so the explicit trusted-token gate is the real defense: a peer
// presenting its own token must NOT be able to register a restriction.
LogosMockSetup mock;
seedTrustedChannel();
seedModule("malicious_module");
seedModule("package_manager");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
// malicious_module tries to grant itself access using its OWN token.
const bool ok = plugin.registerRestriction(
"seed-token-malicious_module", "package_manager",
QStringList{"malicious_module"});
LOGOS_ASSERT_FALSE(ok);
// And an empty token is rejected too.
LOGOS_ASSERT_FALSE(plugin.registerRestriction(
"", "package_manager", QStringList{"malicious_module"}));
}
LOGOS_TEST(requestModule_allows_listed_caller_for_restricted_target) {
LogosMockSetup mock;
seedTrustedChannel();
seedModule("package_manager_ui");
seedModule("package_manager");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
LOGOS_ASSERT_TRUE(plugin.registerRestriction(
kTrustedToken, "package_manager", QStringList{"package_manager_ui"}));
QString token = plugin.requestModule("package_manager_ui", "package_manager");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
}
LOGOS_TEST(requestModule_denies_unlisted_caller_for_restricted_target) {
LogosMockSetup mock;
seedTrustedChannel();
seedModule("some_other_module");
seedModule("package_manager");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
plugin.registerRestriction(kTrustedToken, "package_manager", QStringList{"package_manager_ui"});
// some_other_module is a known, loaded module (passes the identity gate)
// but is not in package_manager's allowed-caller set — must be denied.
QString token = plugin.requestModule("some_other_module", "package_manager");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
LOGOS_TEST(requestModule_allows_any_caller_for_unrestricted_target) {
LogosMockSetup mock;
seedTrustedChannel();
seedModule("some_module");
seedModule("restricted_target");
seedModule("open_target");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
// Restrict only restricted_target; open_target has no restriction.
plugin.registerRestriction(kTrustedToken, "restricted_target", QStringList{"allowed_caller"});
QString token = plugin.requestModule("some_module", "open_target");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
}
LOGOS_TEST(requestModule_allows_all_when_no_restriction_registered) {
// Back-compat: with no policy pushed, every known caller/target pair works.
LogosMockSetup mock;
seedModule("requester_module");
seedModule("target_module");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT_FALSE(token.isEmpty());
}
LOGOS_TEST(registerRestriction_overwrites_previous_for_same_target) {
LogosMockSetup mock;
seedTrustedChannel();
seedModule("old_caller");
seedModule("new_caller");
seedModule("target_module");
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
plugin.initLogos(&api);
plugin.registerRestriction(kTrustedToken, "target_module", QStringList{"old_caller"});
// Re-register (as core does each boot) with a different allowed set.
plugin.registerRestriction(kTrustedToken, "target_module", QStringList{"new_caller"});
// old_caller is no longer allowed; new_caller is.
LOGOS_ASSERT_TRUE(plugin.requestModule("old_caller", "target_module").isEmpty());
LOGOS_ASSERT_FALSE(plugin.requestModule("new_caller", "target_module").isEmpty());
}