// 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 #include #include #include #include #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 genuine auth token a seeded module holds — what it presents to prove its // identity to requestModule (F-002). Matches what getToken(name) returns. QString tokenOf(const QString& name) { return "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(tokenOf("requester_module"), "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 tokens; for (int i = 0; i < 10; ++i) { tokens.insert(plugin.requestModule(tokenOf("requester"), "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(tokenOf("requester_module"), "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(tokenOf("requester"), "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); // A non-loaded identity has no registered token, so no token can prove it — // even a plausible-looking token must not mint anything. QString token = plugin.requestModule(tokenOf("spoofed_module"), "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(tokenOf("requester_module"), "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(tokenOf("requester_module"), "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(tokenOf("package_manager_ui"), "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 with // its own token) but is not in package_manager's allowed-caller set — denied. QString token = plugin.requestModule(tokenOf("some_other_module"), "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(tokenOf("some_module"), "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(tokenOf("requester_module"), "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(tokenOf("old_caller"), "old_caller", "target_module").isEmpty()); LOGOS_ASSERT_FALSE(plugin.requestModule(tokenOf("new_caller"), "new_caller", "target_module").isEmpty()); }