mirror of
https://github.com/logos-co/logos-capability-module.git
synced 2026-08-27 18:51:10 +00:00
feat: capability_module as a universal module (C4)
Rewrites the trust root as a plain Qt-free C++ class over the host-services
veneer, replacing the hand-written Qt plugin that reached into TokenManager
directly. The RPC surface is preserved EXACTLY — lm reports
requestModule(QString,QString) and registerRestriction(QString,QString,
QStringList) identically before and after; only initLogos(LogosAPI*) is gone,
which is the legacy Qt init hook no universal module has.
* getTokenKeys() -> logos::host::tokenKeys() [token_registry]
* getToken(name) -> logos::host::tokenFor(name)
* informModuleToken_module -> logos::host::informModuleTokenTo [token_delivery]
* QHash/QSet -> std::map/std::set, mutex-guarded (the Qt original
was implicitly serialised by the event loop, which
is not a property to inherit silently)
* constantTimeEquals -> the std::string one in logos_host_services.h
Token minting uses boost::uuids::random_generator — deliberately the SAME
generator the host uses for each module's token (liblogos module_manager.cpp),
not a hand-rolled std::random_device formatter: boost seeds from the platform
CSPRNG, while std::random_device is permitted to be deterministic and
historically was on MinGW, which is a live target. This value IS the auth token.
The argument order of the delivery call is spelled out at the call site because
it is the trap: authenticate with the TARGET's token, origin_module is the
TARGET, module_name is the REQUESTER. Swapping the last two compiles and
returns an ok-shaped status while telling the wrong module about the wrong
token.
PROVEN AT RUNTIME: logos-test-modules ipc-tests FAIL -> PASS with this module in
place — a universal trust root minting tokens under a host-granted privilege.
Getting there needed a fix outside this repo. The grant was delivered to the
module's process and then dropped, because module-builder emitted the cdylib
glue with logos-qt-sdk's STALE copy of the generator instead of the maintained
one in logos-plugin-qt (both compile, so nothing failed). The explicit refusal
message this impl logs is what made that findable at all:
[capability_module] REFUSING 'core_service': this module was not granted the
token_registry host service, so it cannot verify any caller
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0cb33fb21c
commit
fc39b1b46e
+6
-3
@@ -9,10 +9,13 @@ else()
|
||||
message(FATAL_ERROR "LogosModule.cmake not found. Set LOGOS_MODULE_BUILDER_ROOT.")
|
||||
endif()
|
||||
|
||||
# Universal module: `interface: "universal"` makes the builder run
|
||||
# logos-cpp-generator --from-header on src/capability_module_impl.h and emit the
|
||||
# Qt plugin glue, the dispatch and the C-ABI exports into generated_code/.
|
||||
# There is no hand-written plugin or interface header any more.
|
||||
logos_module(
|
||||
NAME capability_module
|
||||
SOURCES
|
||||
src/capability_module_interface.h
|
||||
src/capability_module_plugin.h
|
||||
src/capability_module_plugin.cpp
|
||||
src/capability_module_impl.h
|
||||
src/capability_module_impl.cpp
|
||||
)
|
||||
|
||||
+18
-4
@@ -12,16 +12,30 @@
|
||||
"module_coordination",
|
||||
"permission_management"
|
||||
],
|
||||
|
||||
"nix": {
|
||||
"packages": {
|
||||
"build": [],
|
||||
"runtime": []
|
||||
"build": [
|
||||
"boost"
|
||||
],
|
||||
"runtime": [
|
||||
"boost"
|
||||
]
|
||||
},
|
||||
"external_libraries": [],
|
||||
"cmake": {
|
||||
"find_packages": [],
|
||||
"find_packages": [
|
||||
"Boost"
|
||||
],
|
||||
"extra_sources": []
|
||||
}
|
||||
},
|
||||
"interface": "universal",
|
||||
"host_services": [
|
||||
"token_registry",
|
||||
"token_delivery"
|
||||
],
|
||||
"codegen": {
|
||||
"impl_class": "CapabilityModuleImpl",
|
||||
"impl_header": "src/capability_module_impl.h"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#include "capability_module_impl.h"
|
||||
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
|
||||
#include <logos_host_services.h>
|
||||
#include <logos_protocol.h>
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
// The minted value IS the auth token, so this is the one place entropy matters.
|
||||
// Deliberately the SAME generator the host uses to mint each module's token
|
||||
// (logos-liblogos module_manager.cpp), rather than a hand-rolled
|
||||
// std::random_device formatter: boost seeds from the platform CSPRNG
|
||||
// (/dev/urandom, BCryptGenRandom), whereas std::random_device is permitted to
|
||||
// be DETERMINISTIC and historically was on MinGW — which is a live target here.
|
||||
std::string mintToken()
|
||||
{
|
||||
static boost::uuids::random_generator gen;
|
||||
return boost::uuids::to_string(gen());
|
||||
}
|
||||
|
||||
// RAII for the per-target client. lp_client_destroy is safe from any thread and
|
||||
// defers teardown to the owner thread when needed, so an early return cannot
|
||||
// leak the handle.
|
||||
class ClientHandle {
|
||||
public:
|
||||
ClientHandle(const std::string& target, const std::string& origin)
|
||||
: m_c(lp_client_create(target.c_str(), origin.c_str(), nullptr, nullptr)) {}
|
||||
~ClientHandle() { if (m_c) lp_client_destroy(m_c); }
|
||||
ClientHandle(const ClientHandle&) = delete;
|
||||
ClientHandle& operator=(const ClientHandle&) = delete;
|
||||
|
||||
lp_client* get() const { return m_c; }
|
||||
explicit operator bool() const { return m_c != nullptr; }
|
||||
|
||||
private:
|
||||
lp_client* m_c = nullptr;
|
||||
};
|
||||
|
||||
void warn(const char* fmt, const std::string& a, const std::string& b = {})
|
||||
{
|
||||
std::fprintf(stderr, fmt, a.c_str(), b.c_str());
|
||||
}
|
||||
|
||||
// A module that calls out from its own initializer has not published its
|
||||
// source yet, so this one grant can be unsatisfiable. Waiting the protocol
|
||||
// default (20s) there would blow every startup deadline downstream — the
|
||||
// standalone app gives a ui-host 10s to report ready — and turn one
|
||||
// unreachable module into a dead UI. Fail fast instead: the caller gets no
|
||||
// token and a clear reason, and the rest of startup keeps moving.
|
||||
constexpr int kTokenPushTimeoutMs = 3000;
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string CapabilityModuleImpl::requestModule(const std::string& fromModuleName,
|
||||
const std::string& moduleName)
|
||||
{
|
||||
if (fromModuleName.empty() || moduleName.empty()) {
|
||||
warn("[capability_module] rejecting empty module name (from='%s' target='%s')\n",
|
||||
fromModuleName, moduleName);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Known-caller gate. The requesting identity must be one this image has a
|
||||
// token for; fail closed on an unknown name rather than mint a token for a
|
||||
// self-asserted identity that was never loaded.
|
||||
//
|
||||
// This reads the token REGISTRY, so it is the call that makes the
|
||||
// "token_registry" grant load-bearing: ungranted, tokenKeys() returns empty
|
||||
// and every request is refused. That is the correct fail-closed direction,
|
||||
// but it means a missing grant looks exactly like "nothing is loaded" —
|
||||
// hence the explicit status check rather than an `empty()` test.
|
||||
logos::host::Status keysStatus;
|
||||
const std::vector<std::string> known = logos::host::tokenKeys(&keysStatus);
|
||||
if (keysStatus.ungranted()) {
|
||||
warn("[capability_module] REFUSING '%s': this module was not granted the "
|
||||
"token_registry host service, so it cannot verify any caller\n",
|
||||
fromModuleName);
|
||||
return {};
|
||||
}
|
||||
bool callerKnown = false;
|
||||
for (const std::string& k : known) {
|
||||
if (k == fromModuleName) { callerKnown = true; break; }
|
||||
}
|
||||
if (!callerKnown) {
|
||||
warn("[capability_module] rejecting request from unknown module identity '%s' "
|
||||
"- no token registered for it\n", fromModuleName);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Known-target gate: no token for the target means it is not loaded. Don't
|
||||
// hand back a token the target would reject anyway.
|
||||
const std::string moduleToken = logos::host::tokenFor(moduleName);
|
||||
if (moduleToken.empty()) {
|
||||
warn("[capability_module] rejecting request for unknown target '%s' "
|
||||
"- no token registered for it\n", moduleName);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Access-policy gate.
|
||||
//
|
||||
// TODO(access-policy): still fail-OPEN — a target with no registered
|
||||
// restriction is unrestricted. Intentional for back-compat during rollout;
|
||||
// the end state is deny-by-default once every deployment ships a policy.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
auto it = m_restrictions.find(moduleName);
|
||||
if (it != m_restrictions.end() && it->second.count(fromModuleName) == 0) {
|
||||
warn("[capability_module] access policy denies '%s' -> '%s'\n",
|
||||
fromModuleName, moduleName);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const std::string authToken = mintToken();
|
||||
|
||||
ClientHandle client(moduleName, "capability_module");
|
||||
if (!client) {
|
||||
warn("[capability_module] could not create a client for target '%s'\n", moduleName);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Deliver the token for the REQUESTER to the TARGET.
|
||||
//
|
||||
// The argument order is the trap here, so spell it out: authenticate with
|
||||
// the TARGET's own token, `originModule` is the TARGET (the module being
|
||||
// told), and `moduleName` is the REQUESTER (the module the token is FOR).
|
||||
// Swapping the last two still compiles and still returns an ok-shaped
|
||||
// status; it just tells the wrong module about the wrong token.
|
||||
const logos::host::Status pushed = logos::host::informModuleTokenTo(
|
||||
client.get(),
|
||||
/*authToken=*/moduleToken,
|
||||
/*originModule=*/moduleName,
|
||||
/*moduleName=*/fromModuleName,
|
||||
/*token=*/authToken,
|
||||
kTokenPushTimeoutMs);
|
||||
|
||||
if (!pushed) {
|
||||
if (pushed.ungranted()) {
|
||||
warn("[capability_module] REFUSING '%s': this module was not granted the "
|
||||
"token_delivery host service, so it cannot push tokens\n", moduleName);
|
||||
} else {
|
||||
warn("[capability_module] failed to inform '%s' about the token for '%s'\n",
|
||||
moduleName, fromModuleName);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
return authToken;
|
||||
}
|
||||
|
||||
bool CapabilityModuleImpl::registerRestriction(const std::string& authToken,
|
||||
const std::string& targetModule,
|
||||
const std::vector<std::string>& allowedCallers)
|
||||
{
|
||||
// Trusted-channel gate: only core (or this module) may rewrite the policy.
|
||||
// Both hold this module's auth token; a peer knows only its own. The
|
||||
// generic authorization that fronts this method accepts ANY issued token,
|
||||
// which would otherwise let any module rewrite the policy.
|
||||
//
|
||||
// constantTimeEquals, not ==: comparing a secret with == leaks the length
|
||||
// of the matching prefix through timing.
|
||||
const std::string coreToken = logos::host::tokenFor("core");
|
||||
const std::string capToken = logos::host::tokenFor("capability_module");
|
||||
const bool callerIsTrusted =
|
||||
(!coreToken.empty() && logos::host::constantTimeEquals(authToken, coreToken)) ||
|
||||
(!capToken.empty() && logos::host::constantTimeEquals(authToken, capToken));
|
||||
if (authToken.empty() || !callerIsTrusted) {
|
||||
warn("[capability_module] rejecting restriction for '%s' - caller is not the "
|
||||
"trusted core channel\n", targetModule);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetModule.empty()) {
|
||||
warn("[capability_module] rejecting empty target module%s\n", std::string());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Overwrite any previous restriction — core is the single source of truth
|
||||
// and re-registers the full set each boot.
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_restrictions[targetModule] =
|
||||
std::set<std::string>(allowedCallers.begin(), allowedCallers.end());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// capability_module — the trust root, as an ordinary universal module.
|
||||
//
|
||||
// It mints per-(caller, target) auth tokens and pushes them to the target, and
|
||||
// it holds the access policy core registers. That needs two privileges no other
|
||||
// module has: enumerating the token store, and delivering a token to an
|
||||
// arbitrary module. It declares them in metadata.json:
|
||||
//
|
||||
// "host_services": ["token_registry", "token_delivery"]
|
||||
//
|
||||
// and the HOST grants them, bound to this module's verified name. Ungranted,
|
||||
// every gated call fails closed — and note that is not a partial degradation:
|
||||
// the known-caller check below reads the token registry, so an ungranted
|
||||
// capability_module refuses EVERY requestModule.
|
||||
//
|
||||
// No Qt: this is a plain C++ class the generator turns into a module. The Qt
|
||||
// plugin it used to be reached TokenManager directly, which is exactly the
|
||||
// ambient privilege the host-services grant replaces.
|
||||
//
|
||||
// NO trailing `// comments` on declaration lines (the parser needs a `;`).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <logos_module_context.h>
|
||||
|
||||
class CapabilityModuleImpl : public LogosModuleContext {
|
||||
public:
|
||||
CapabilityModuleImpl() = default;
|
||||
~CapabilityModuleImpl() = default;
|
||||
|
||||
// Mint a token letting `fromModuleName` call `moduleName`, push it to the
|
||||
// target, and return it. Empty string on any refusal — an unknown caller,
|
||||
// an unknown target, a policy denial, or an unreachable target.
|
||||
std::string requestModule(const std::string& fromModuleName,
|
||||
const std::string& moduleName);
|
||||
|
||||
// Restrict `targetModule` to `allowedCallers`. Only core (or this module)
|
||||
// may call this; the check is a constant-time comparison against their
|
||||
// tokens. Re-registering a target replaces its previous set.
|
||||
bool registerRestriction(const std::string& authToken,
|
||||
const std::string& targetModule,
|
||||
const std::vector<std::string>& allowedCallers);
|
||||
|
||||
private:
|
||||
// target -> callers permitted to reach it. A target absent from the map is
|
||||
// unrestricted; see the fail-open note in the .cpp.
|
||||
//
|
||||
// Guarded because a universal module may be dispatched concurrently if it
|
||||
// ever declares concurrency:"multi", and because this is policy state — the
|
||||
// Qt original was implicitly serialised by the event loop, which is not a
|
||||
// property to inherit silently.
|
||||
std::map<std::string, std::set<std::string>> m_restrictions;
|
||||
mutable std::mutex m_mutex;
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
#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
|
||||
@@ -1,172 +0,0 @@
|
||||
#include "capability_module_plugin.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "logos_api_client.h"
|
||||
#include "token_manager.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// 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();
|
||||
const QByteArray bb = b.toUtf8();
|
||||
const int n = std::max(ba.size(), bb.size());
|
||||
int diff = ba.size() ^ bb.size();
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const unsigned char ca = i < ba.size() ? static_cast<unsigned char>(ba[i]) : 0;
|
||||
const unsigned char cb = i < bb.size() ? static_cast<unsigned char>(bb[i]) : 0;
|
||||
diff |= (ca ^ cb);
|
||||
}
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CapabilityModulePlugin::CapabilityModulePlugin(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
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;
|
||||
|
||||
if (!logosAPI) {
|
||||
qWarning() << "CapabilityModulePlugin::requestModule: LogosAPI not initialized";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (fromModuleName.isEmpty() || moduleName.isEmpty()) {
|
||||
qWarning() << "CapabilityModulePlugin::requestModule: rejecting empty module name"
|
||||
<< "(fromModuleName / moduleName must both be set)";
|
||||
return {};
|
||||
}
|
||||
|
||||
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() << "CapabilityModulePlugin::requestModule: rejecting request from unknown"
|
||||
<< "module identity:" << fromModuleName
|
||||
<< "- no token registered for it (unverified requesting identity)";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Known-target gate: an empty target token means the target is not loaded /
|
||||
// unknown. Don't hand back a token the target would reject anyway — fail closed.
|
||||
const QString moduleToken = tokenManager->getToken(moduleName);
|
||||
if (moduleToken.isEmpty()) {
|
||||
qWarning() << "CapabilityModulePlugin::requestModule: rejecting request for unknown"
|
||||
<< "target module:" << moduleName << "- no token registered for it";
|
||||
return {};
|
||||
}
|
||||
|
||||
// Access-policy gate: if core registered a restriction for this target,
|
||||
// only the listed callers may obtain a token. A target with no registered
|
||||
// restriction is unrestricted (back-compat). Fail closed for a restricted
|
||||
// target — the denied caller never gets a token, so it can never call it.
|
||||
//
|
||||
// TODO(access-policy): this is currently fail-OPEN — when m_restrictions is
|
||||
// empty (no policy pushed, e.g. running against a core that doesn't push
|
||||
// restrictions) nothing is enforced and every caller is allowed. This is
|
||||
// intentional for back-compat during rollout, but the end state should be
|
||||
// deny-by-default: once every deployment ships a policy, an empty/unknown
|
||||
// policy should block inter-module calls rather than permit them. Revisit
|
||||
// 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() << "CapabilityModulePlugin::requestModule: access policy denies"
|
||||
<< fromModuleName << "->" << moduleName
|
||||
<< "- caller not in the allowed set";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const QString authTokenString = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
qDebug() << "CapabilityModulePlugin: Calling informModuleToken on target module:" << moduleName;
|
||||
|
||||
// Bounded push. The target must be reachable to be told about the token,
|
||||
// which means its source has to be published. A module that calls out from
|
||||
// its own initializer has not published yet (older SDKs publish only after
|
||||
// the initializer returns), so this can be the one grant that cannot be
|
||||
// satisfied. Waiting the default 20s there would blow every startup deadline
|
||||
// downstream — the standalone app gives a ui-host 10s to report ready — and
|
||||
// turn one unreachable module into a dead UI. Fail fast instead: the caller
|
||||
// gets no token and a clear reason, and the rest of startup keeps moving.
|
||||
static constexpr int kTokenPushTimeoutMs = 3000;
|
||||
const bool success = logosAPI->getClient(moduleName)->informModuleToken_module(
|
||||
moduleToken, moduleName, fromModuleName, authTokenString, kTokenPushTimeoutMs);
|
||||
if (!success) {
|
||||
qWarning() << "CapabilityModulePlugin: Failed to inform" << moduleName
|
||||
<< "about token for" << fromModuleName;
|
||||
return {};
|
||||
}
|
||||
|
||||
qDebug() << "CapabilityModulePlugin: Successfully informed" << moduleName
|
||||
<< "about token for" << fromModuleName;
|
||||
return authTokenString;
|
||||
}
|
||||
|
||||
bool CapabilityModulePlugin::registerRestriction(const QString& authToken,
|
||||
const QString& targetModule,
|
||||
const QStringList& allowedCallers)
|
||||
{
|
||||
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. The
|
||||
// generic isAuthorized() that fronts this method accepts ANY issued
|
||||
// token, which would otherwise let a malicious module rewrite the policy.
|
||||
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() << "CapabilityModulePlugin::registerRestriction: rejecting restriction for"
|
||||
<< targetModule << "- caller is not the trusted core channel";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetModule.isEmpty()) {
|
||||
qWarning() << "CapabilityModulePlugin::registerRestriction: rejecting empty target module";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Overwrite any previous restriction for this target — core is the
|
||||
// single source of truth and re-registers the full set each boot.
|
||||
m_restrictions.insert(targetModule, QSet<QString>(allowedCallers.begin(), allowedCallers.end()));
|
||||
|
||||
qDebug() << "CapabilityModulePlugin::registerRestriction: target" << targetModule
|
||||
<< "restricted to callers" << allowedCallers;
|
||||
return true;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user