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
This commit is contained in:
Iuri Matias
2026-06-11 16:31:00 -04:00
parent 5d438961e2
commit 22e54ffc79
11 changed files with 5442 additions and 247 deletions
+3 -3
View File
@@ -12,7 +12,7 @@ endif()
logos_module(
NAME capability_module
SOURCES
src/capability_module_loader.h
src/capability_module_impl.h
src/capability_module_impl.cpp
src/capability_module_interface.h
src/capability_module_plugin.h
src/capability_module_plugin.cpp
)
Generated
+5249 -91
View File
File diff suppressed because it is too large Load Diff
+11 -4
View File
@@ -6,9 +6,16 @@
};
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
let
module = logos-module-builder.lib.mkLogosModule {
src = ./.;
configFile = ./metadata.json;
flakeInputs = inputs;
tests = {
dir = ./tests;
};
};
in module // {
checks = module.checks or {};
};
}
-1
View File
@@ -4,7 +4,6 @@
"description": "Coordinates permissions between modules",
"author": "Logos Core Team",
"type": "core",
"interface": "provider",
"category": "security",
"main": "capability_module_plugin",
"dependencies": [],
-37
View File
@@ -1,37 +0,0 @@
#ifndef CAPABILITY_MODULE_IMPL_H
#define CAPABILITY_MODULE_IMPL_H
#include <QHash>
#include <QSet>
#include <QString>
#include <QStringList>
#include "logos_provider_object.h"
class CapabilityModuleImpl : public LogosProviderBase
{
LOGOS_PROVIDER(CapabilityModuleImpl, "capability_module", "1.0.0")
public:
LOGOS_METHOD QString requestModule(const QString& fromModuleName, const QString& moduleName);
// Register an access restriction: only the listed caller modules may
// obtain a token for (and therefore call) `targetModule`. Called by
// core after it parses the access policy.
//
// `authToken` must be the trusted core/capability_module auth token —
// the same gate informModuleToken uses. ModuleProxy::isAuthorized lets
// ANY loaded module reach this method (it accepts any issued token), so
// without this explicit check a malicious module could relax or
// overwrite restrictions to grant itself access. Only core holds the
// trusted token, so peers cannot forge it. Returns true on success.
LOGOS_METHOD bool registerRestriction(const QString& authToken, const QString& targetModule, const QStringList& allowedCallers);
private:
// target module -> set of caller modules allowed to reach it. A target
// absent from this map is unrestricted (back-compat default). Populated
// only by registerRestriction; consulted in requestModule.
QHash<QString, QSet<QString>> m_restrictions;
};
#endif // CAPABILITY_MODULE_IMPL_H
+34
View File
@@ -0,0 +1,34 @@
#ifndef CAPABILITY_MODULE_INTERFACE_H
#define CAPABILITY_MODULE_INTERFACE_H
#include <QString>
#include <QStringList>
#include "interface.h"
// Public API of capability_module. It is internal-layer infrastructure: a
// module developer never calls these directly — requestModule is invoked under
// the hood by the SDK when one module first calls another, and
// registerRestriction is invoked by core when it installs the access policy.
class CapabilityModuleInterface : public PluginInterface
{
public:
virtual ~CapabilityModuleInterface() = default;
// Mint an auth token allowing `fromModuleName` to call `moduleName`, inform
// the target of the new token, and return it to the caller.
Q_INVOKABLE virtual QString requestModule(const QString& fromModuleName,
const QString& moduleName) = 0;
// Register an access restriction: only the listed caller modules may obtain
// a token for (and therefore call) `targetModule`. `authToken` must be the
// trusted core/capability_module token.
Q_INVOKABLE virtual bool registerRestriction(const QString& authToken,
const QString& targetModule,
const QStringList& allowedCallers) = 0;
};
#define CapabilityModuleInterface_iid "org.logos.CapabilityModuleInterface"
Q_DECLARE_INTERFACE(CapabilityModuleInterface, CapabilityModuleInterface_iid)
#endif // CAPABILITY_MODULE_INTERFACE_H
-22
View File
@@ -1,22 +0,0 @@
#ifndef CAPABILITY_MODULE_LOADER_H
#define CAPABILITY_MODULE_LOADER_H
#include <QObject>
#include "capability_module_impl.h"
#include "interface.h"
#include "logos_provider_object.h"
class CapabilityModuleLoader : public QObject, public PluginInterface, public LogosProviderPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID LogosProviderPlugin_iid FILE "metadata.json")
Q_INTERFACES(PluginInterface LogosProviderPlugin)
public:
QString name() const override { return QStringLiteral("capability_module"); }
QString version() const override { return QStringLiteral("1.0.0"); }
LogosProviderObject* createProviderObject() override { return new CapabilityModuleImpl(); }
};
#endif // CAPABILITY_MODULE_LOADER_H
@@ -1,4 +1,4 @@
#include "capability_module_impl.h"
#include "capability_module_plugin.h"
#include <QByteArray>
#include <QDebug>
@@ -6,15 +6,14 @@
#include <algorithm>
#include "logos_api.h"
#include "logos_api_client.h"
#include "token_manager.h"
namespace {
// Constant-time token comparison (mirrors ModuleProxy's): compare over the
// longer of the two lengths and fold any length difference into the result,
// so neither a correct prefix nor the secret's length leaks through timing.
// Constant-time token comparison: compare over the longer of the two lengths
// and fold any length difference into the result, so neither a correct prefix
// nor the secret's length leaks through timing.
bool constantTimeEquals(const QString& a, const QString& b)
{
const QByteArray ba = a.toUtf8();
@@ -31,30 +30,46 @@ bool constantTimeEquals(const QString& a, const QString& b)
} // namespace
QString CapabilityModuleImpl::requestModule(const QString& fromModuleName, const QString& moduleName)
CapabilityModulePlugin::CapabilityModulePlugin(QObject* parent)
: QObject(parent)
{
qDebug() << "CapabilityModuleImpl::requestModule called with fromModuleName:" << fromModuleName
qDebug() << "CapabilityModulePlugin: created";
}
CapabilityModulePlugin::~CapabilityModulePlugin()
{
qDebug() << "CapabilityModulePlugin: destroyed";
}
void CapabilityModulePlugin::initLogos(LogosAPI* logosAPIInstance)
{
logosAPI = logosAPIInstance;
qDebug() << "CapabilityModulePlugin: LogosAPI initialized";
}
QString CapabilityModulePlugin::requestModule(const QString& fromModuleName, const QString& moduleName)
{
qDebug() << "CapabilityModulePlugin::requestModule called with fromModuleName:" << fromModuleName
<< "moduleName:" << moduleName;
LogosAPI* api = logosAPI();
if (!api) {
qWarning() << "CapabilityModuleImpl::requestModule: LogosAPI not initialized";
if (!logosAPI) {
qWarning() << "CapabilityModulePlugin::requestModule: LogosAPI not initialized";
return {};
}
if (fromModuleName.isEmpty() || moduleName.isEmpty()) {
qWarning() << "CapabilityModuleImpl::requestModule: rejecting empty module name"
qWarning() << "CapabilityModulePlugin::requestModule: rejecting empty module name"
<< "(fromModuleName / moduleName must both be set)";
return {};
}
TokenManager* tokenManager = api->getTokenManager();
TokenManager* tokenManager = logosAPI->getTokenManager();
// Known-caller gate: the requesting identity must be a module capability_module
// already knows about. Fail closed on an unknown name rather than mint a token
// for a self-asserted identity that was never loaded.
if (!tokenManager->getTokenKeys().contains(fromModuleName)) {
qWarning() << "CapabilityModuleImpl::requestModule: rejecting request from unknown"
qWarning() << "CapabilityModulePlugin::requestModule: rejecting request from unknown"
<< "module identity:" << fromModuleName
<< "- no token registered for it (unverified requesting identity)";
return {};
@@ -64,7 +79,7 @@ QString CapabilityModuleImpl::requestModule(const QString& fromModuleName, const
// unknown. Don't hand back a token the target would reject anyway — fail closed.
const QString moduleToken = tokenManager->getToken(moduleName);
if (moduleToken.isEmpty()) {
qWarning() << "CapabilityModuleImpl::requestModule: rejecting request for unknown"
qWarning() << "CapabilityModulePlugin::requestModule: rejecting request for unknown"
<< "target module:" << moduleName << "- no token registered for it";
return {};
}
@@ -83,7 +98,7 @@ QString CapabilityModuleImpl::requestModule(const QString& fromModuleName, const
// and flip the default once the policy is guaranteed to be present.
if (auto it = m_restrictions.constFind(moduleName); it != m_restrictions.constEnd()) {
if (!it->contains(fromModuleName)) {
qWarning() << "CapabilityModuleImpl::requestModule: access policy denies"
qWarning() << "CapabilityModulePlugin::requestModule: access policy denies"
<< fromModuleName << "->" << moduleName
<< "- caller not in the allowed set";
return {};
@@ -92,51 +107,49 @@ QString CapabilityModuleImpl::requestModule(const QString& fromModuleName, const
const QString authTokenString = QUuid::createUuid().toString(QUuid::WithoutBraces);
qDebug() << "CapabilityModuleImpl: Calling informModuleToken on target module:" << moduleName;
qDebug() << "CapabilityModulePlugin: Calling informModuleToken on target module:" << moduleName;
const bool success = api->getClient(moduleName)->informModuleToken_module(
const bool success = logosAPI->getClient(moduleName)->informModuleToken_module(
moduleToken, moduleName, fromModuleName, authTokenString);
if (!success) {
qWarning() << "CapabilityModuleImpl: Failed to inform" << moduleName
qWarning() << "CapabilityModulePlugin: Failed to inform" << moduleName
<< "about token for" << fromModuleName;
return {};
}
qDebug() << "CapabilityModuleImpl: Successfully informed" << moduleName
qDebug() << "CapabilityModulePlugin: Successfully informed" << moduleName
<< "about token for" << fromModuleName;
return authTokenString;
}
bool CapabilityModuleImpl::registerRestriction(const QString& authToken,
const QString& targetModule,
const QStringList& allowedCallers)
bool CapabilityModulePlugin::registerRestriction(const QString& authToken,
const QString& targetModule,
const QStringList& allowedCallers)
{
LogosAPI* api = logosAPI();
if (!api) {
qWarning() << "CapabilityModuleImpl::registerRestriction: LogosAPI not initialized";
if (!logosAPI) {
qWarning() << "CapabilityModulePlugin::registerRestriction: LogosAPI not initialized";
return false;
}
// Trusted-channel gate: only core (or capability_module itself) may
// register restrictions. Both hold capability_module's auth token; a
// peer module only knows its own token and so cannot forge this. Same
// check ModuleProxy::informModuleToken makes — necessary because the
// peer module only knows its own token and so cannot forge this. The
// generic isAuthorized() that fronts this method accepts ANY issued
// token, which would otherwise let a malicious module rewrite the policy.
TokenManager* tokenManager = api->getTokenManager();
TokenManager* tokenManager = logosAPI->getTokenManager();
const QString coreToken = tokenManager->getToken(QStringLiteral("core"));
const QString capToken = tokenManager->getToken(QStringLiteral("capability_module"));
const bool callerIsTrusted =
(!coreToken.isEmpty() && constantTimeEquals(authToken, coreToken)) ||
(!capToken.isEmpty() && constantTimeEquals(authToken, capToken));
if (authToken.isEmpty() || !callerIsTrusted) {
qWarning() << "CapabilityModuleImpl::registerRestriction: rejecting restriction for"
qWarning() << "CapabilityModulePlugin::registerRestriction: rejecting restriction for"
<< targetModule << "- caller is not the trusted core channel";
return false;
}
if (targetModule.isEmpty()) {
qWarning() << "CapabilityModuleImpl::registerRestriction: rejecting empty target module";
qWarning() << "CapabilityModulePlugin::registerRestriction: rejecting empty target module";
return false;
}
@@ -144,7 +157,7 @@ bool CapabilityModuleImpl::registerRestriction(const QString& authToken,
// single source of truth and re-registers the full set each boot.
m_restrictions.insert(targetModule, QSet<QString>(allowedCallers.begin(), allowedCallers.end()));
qDebug() << "CapabilityModuleImpl::registerRestriction: target" << targetModule
qDebug() << "CapabilityModulePlugin::registerRestriction: target" << targetModule
<< "restricted to callers" << allowedCallers;
return true;
}
+42
View File
@@ -0,0 +1,42 @@
#ifndef CAPABILITY_MODULE_PLUGIN_H
#define CAPABILITY_MODULE_PLUGIN_H
#include <QHash>
#include <QObject>
#include <QSet>
#include <QString>
#include <QStringList>
#include "capability_module_interface.h"
#include "logos_api.h"
class CapabilityModulePlugin : public QObject, public CapabilityModuleInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID CapabilityModuleInterface_iid FILE "metadata.json")
Q_INTERFACES(CapabilityModuleInterface PluginInterface)
public:
explicit CapabilityModulePlugin(QObject* parent = nullptr);
~CapabilityModulePlugin() override;
QString name() const override { return QStringLiteral("capability_module"); }
QString version() const override { return QStringLiteral("1.0.0"); }
Q_INVOKABLE void initLogos(LogosAPI* logosAPIInstance);
Q_INVOKABLE QString requestModule(const QString& fromModuleName,
const QString& moduleName) override;
Q_INVOKABLE bool registerRestriction(const QString& authToken,
const QString& targetModule,
const QStringList& allowedCallers) override;
private:
// target module -> set of caller modules allowed to reach it. A target
// absent from this map is unrestricted (back-compat default). Populated
// only by registerRestriction; consulted in requestModule.
QHash<QString, QSet<QString>> m_restrictions;
};
#endif // CAPABILITY_MODULE_PLUGIN_H
+1 -5
View File
@@ -6,12 +6,8 @@ include(LogosTest)
logos_test(
NAME capability_module_tests
MODULE_SOURCES
../src/capability_module_impl.cpp
../src/capability_module_plugin.cpp
TEST_SOURCES
main.cpp
test_capability_module.cpp
GENERATED_SOURCES
../generated_code/logos_provider_dispatch.cpp
GENERATED_DIR
../generated_code
)
+58 -53
View File
@@ -1,4 +1,8 @@
// Unit tests for CapabilityModuleImpl.
// 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.
@@ -26,8 +30,9 @@
#include <QSet>
#include <QString>
#include "capability_module_impl.h"
#include "capability_module_plugin.h"
#include "logos_api.h"
#include "token_manager.h"
namespace {
@@ -60,11 +65,11 @@ LOGOS_TEST(requestModule_returns_uuid_format_token) {
seedModule("requester_module");
seedModule("target_module");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
QString token = impl.requestModule("requester_module", "target_module");
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
@@ -75,13 +80,13 @@ LOGOS_TEST(requestModule_mints_unique_token_per_call) {
seedModule("requester");
seedModule("target");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
QSet<QString> tokens;
for (int i = 0; i < 10; ++i) {
tokens.insert(impl.requestModule("requester", "target"));
tokens.insert(plugin.requestModule("requester", "target"));
}
LOGOS_ASSERT_EQ(tokens.size(), 10);
@@ -94,11 +99,11 @@ LOGOS_TEST(requestModule_works_when_target_token_is_pre_seeded) {
seedModule("requester_module");
TokenManager::instance().saveToken("target_module", "pre-seeded-token");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
QString token = impl.requestModule("requester_module", "target_module");
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
}
@@ -107,9 +112,9 @@ LOGOS_TEST(requestModule_works_when_target_token_is_pre_seeded) {
LOGOS_TEST(requestModule_returns_empty_when_not_initialized) {
LogosMockSetup mock;
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
QString token = impl.requestModule("requester", "target");
QString token = plugin.requestModule("requester", "target");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
@@ -118,11 +123,11 @@ LOGOS_TEST(requestModule_rejects_empty_fromModuleName) {
LogosMockSetup mock;
seedModule("target_module");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
QString token = impl.requestModule("", "target_module");
QString token = plugin.requestModule("", "target_module");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
@@ -132,12 +137,12 @@ LOGOS_TEST(requestModule_rejects_unknown_fromModuleName) {
// Only the target is known; the requesting identity was never loaded.
seedModule("target_module");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
// Spoofing a non-loaded identity must not mint a token.
QString token = impl.requestModule("spoofed_module", "target_module");
QString token = plugin.requestModule("spoofed_module", "target_module");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
@@ -147,11 +152,11 @@ LOGOS_TEST(requestModule_rejects_unknown_target) {
// Only the caller is known; the target was never loaded.
seedModule("requester_module");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
QString token = impl.requestModule("requester_module", "missing_target");
QString token = plugin.requestModule("requester_module", "missing_target");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
@@ -161,11 +166,11 @@ LOGOS_TEST(requestModule_succeeds_for_known_caller_and_target) {
seedModule("requester_module");
seedModule("target_module");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
QString token = impl.requestModule("requester_module", "target_module");
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
@@ -182,11 +187,11 @@ LOGOS_TEST(requestModule_succeeds_for_known_caller_and_target) {
LOGOS_TEST(registerRestriction_rejects_empty_target) {
LogosMockSetup mock;
seedTrustedChannel();
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
LOGOS_ASSERT_FALSE(impl.registerRestriction(kTrustedToken, "", QStringList{"caller"}));
LOGOS_ASSERT_FALSE(plugin.registerRestriction(kTrustedToken, "", QStringList{"caller"}));
}
LOGOS_TEST(registerRestriction_rejects_untrusted_caller_token) {
@@ -198,18 +203,18 @@ LOGOS_TEST(registerRestriction_rejects_untrusted_caller_token) {
seedModule("malicious_module");
seedModule("package_manager");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
// malicious_module tries to grant itself access using its OWN token.
const bool ok = impl.registerRestriction(
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(impl.registerRestriction(
LOGOS_ASSERT_FALSE(plugin.registerRestriction(
"", "package_manager", QStringList{"malicious_module"}));
}
@@ -219,14 +224,14 @@ LOGOS_TEST(requestModule_allows_listed_caller_for_restricted_target) {
seedModule("package_manager_ui");
seedModule("package_manager");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
LOGOS_ASSERT_TRUE(impl.registerRestriction(
LOGOS_ASSERT_TRUE(plugin.registerRestriction(
kTrustedToken, "package_manager", QStringList{"package_manager_ui"}));
QString token = impl.requestModule("package_manager_ui", "package_manager");
QString token = plugin.requestModule("package_manager_ui", "package_manager");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
@@ -238,15 +243,15 @@ LOGOS_TEST(requestModule_denies_unlisted_caller_for_restricted_target) {
seedModule("some_other_module");
seedModule("package_manager");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
impl.registerRestriction(kTrustedToken, "package_manager", QStringList{"package_manager_ui"});
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 = impl.requestModule("some_other_module", "package_manager");
QString token = plugin.requestModule("some_other_module", "package_manager");
LOGOS_ASSERT_TRUE(token.isEmpty());
}
@@ -258,14 +263,14 @@ LOGOS_TEST(requestModule_allows_any_caller_for_unrestricted_target) {
seedModule("restricted_target");
seedModule("open_target");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
// Restrict only restricted_target; open_target has no restriction.
impl.registerRestriction(kTrustedToken, "restricted_target", QStringList{"allowed_caller"});
plugin.registerRestriction(kTrustedToken, "restricted_target", QStringList{"allowed_caller"});
QString token = impl.requestModule("some_module", "open_target");
QString token = plugin.requestModule("some_module", "open_target");
LOGOS_ASSERT_FALSE(token.isEmpty());
LOGOS_ASSERT(kUuidRegex.match(token).hasMatch());
@@ -277,11 +282,11 @@ LOGOS_TEST(requestModule_allows_all_when_no_restriction_registered) {
seedModule("requester_module");
seedModule("target_module");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
QString token = impl.requestModule("requester_module", "target_module");
QString token = plugin.requestModule("requester_module", "target_module");
LOGOS_ASSERT_FALSE(token.isEmpty());
}
@@ -293,15 +298,15 @@ LOGOS_TEST(registerRestriction_overwrites_previous_for_same_target) {
seedModule("new_caller");
seedModule("target_module");
CapabilityModuleImpl impl;
CapabilityModulePlugin plugin;
LogosAPI api("capability_module");
impl.init(&api);
plugin.initLogos(&api);
impl.registerRestriction(kTrustedToken, "target_module", QStringList{"old_caller"});
plugin.registerRestriction(kTrustedToken, "target_module", QStringList{"old_caller"});
// Re-register (as core does each boot) with a different allowed set.
impl.registerRestriction(kTrustedToken, "target_module", QStringList{"new_caller"});
plugin.registerRestriction(kTrustedToken, "target_module", QStringList{"new_caller"});
// old_caller is no longer allowed; new_caller is.
LOGOS_ASSERT_TRUE(impl.requestModule("old_caller", "target_module").isEmpty());
LOGOS_ASSERT_FALSE(impl.requestModule("new_caller", "target_module").isEmpty());
LOGOS_ASSERT_TRUE(plugin.requestModule("old_caller", "target_module").isEmpty());
LOGOS_ASSERT_FALSE(plugin.requestModule("new_caller", "target_module").isEmpty());
}