mirror of
https://github.com/logos-co/logos-capability-module.git
synced 2026-08-31 04:31:15 +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>
* chore(deps): rev-pin logos-module-builder at the builder this module needs
Second lock pass of the capability-module <-> module-builder <-> standalone-app
cycle. Pass 1 pushed the C4 universal rewrite against a builder that predates
it; this closes the loop.
logos-module-builder 9d3b7cc -> c60d4a9 feat/sdk-codegen-b4-qt-host-repoint
The rev goes in the URL, not just the lock. c60d4a9 is a BRANCH tip, so the
plain `github:logos-co/logos-module-builder` url this repo had would let
`nix flake update` relock onto master (9ac3a15) and silently undo the bump.
Two commits that only exist past master are load-bearing here:
85dfb34 parse and validate metadata.json#host_services (C2) — this module
declares host_services [token_registry, token_delivery], and without
the parser the veneer header is never wired in
ed50731 emit cdylib glue with logos-qt-host-generator, not qt-sdk's stale
copy — the out-of-repo fix this repo's own C4 commit names as its
prerequisite
MEASURED, not assumed. Same tree (fc39b1b), old pin vs new pin:
packages.default 9d3b7cc: EXIT=1 fatal error: 'logos_host_services.h'
file not found (capability_module_impl.cpp:7)
c60d4a9: EXIT=0 /nix/store/fhshfnr5p5icb7bs3ybk0dlj9f7zqxdc-...
lib, generate, include, install and lgx are green at the new pin too (6/7).
The bump is a fast-forward: 9ac3a15 (master) is an ancestor of c60d4a9, and
9d3b7cc is an ancestor of 9ac3a15, so nothing on master is given up. Drop the
rev once the branch merges.
Every transitive rev this drags in was verified to be a live remote branch tip
before it was written here: logos-protocol c8bab12, logos-cpp-sdk a04b278,
logos-qt-sdk 8a06b87, logos-plugin-qt AND logos-plugin-core cc24fa1 (both, per
the type:core / ui split), logos-view-module-runtime 5510acd,
logos-standalone-app 39f4f2b, logos-test-framework c382ab1.
NOT fixed here, and pre-existing: checks.unit-tests fails at both pins with
"Cannot find source file: ../src/capability_module_plugin.cpp". fc39b1b deleted
the hand-written Qt plugin, and tests/ still compiles it and drives
initLogos()/TokenManager. That is a C4 follow-up, not a lock question — see the
level report.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: describe the module this repo actually builds
docs.md still described the `interface: "provider"` shape this module was
migrated off. Every claim below was checked against the tree or the build
output, not against the commit history:
* "Implemented as a LogosProviderBase subclass" — it derives
LogosModuleContext, and the impl is Qt-FREE: zero Qt types in either of its
translation units.
* "exposes a single primary method via the LOGOS_METHOD marker" — there is no
marker; the public methods ARE the API and the generator derives the
contract from the header. There are also TWO of them: registerRestriction
was missing from the table entirely.
* `requestModule → QString` — it returns std::string.
* "LogosAPI* is delivered via LogosProviderBase::onInit" — it reaches the
token store and delivery path through logos_host_services.h, the veneer over
lp_token_keys / lp_inform_module_token_to, gated by the host-services grant.
* A `src/capability_module_loader.h` in the file tree — that file does not
exist; the plugin entry point is generated.
* "generated_code/logos_provider_dispatch.cpp … produced by
logos-cpp-generator --provider-header" — neither the file nor the flag
exists. Confirmed by building
`.#logos-capability-module--capability_module-generate`: what lands is
capability_module.lidl, capability_module_cdylib_glue.{h,cpp},
capability_module_module_impl.cpp and capability_module_types.h.
* `interface: provider` in the metadata section — it is `universal`.
The three-step build pipeline is now spelled out with what each step produces,
since "which file comes from where" was the thing the old text got wrong in
every particular.
The removed path is named as history rather than deleted outright: "there used
to be a LOGOS_METHOD dispatch" is the question a reader arrives with, and
leaving no trace of it invites the same confusion in reverse.
Found while sweeping for stale references to the deleted generator paths after
the same class of rot turned up in logos-cpp-sdk's docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: correct the capability_module docs for the universal migration
docs.md and the README still described this module as a handcrafted Qt plugin
and, worse, claimed the request path "always grants requests". It does not:
requestModule gates on a known caller (logos::host::tokenKeys()), a known
target (tokenFor()), and the registerRestriction access policy, minting the
token only after those pass. The token-flow section had the ordering backwards
and routed the push through LogosAPIClient, a Qt type this now Qt-free impl
cannot use — it goes through logos::host::informModuleTokenTo() over an
lp_client.
src/capability_module.lidl is marked DEAD: the module publishes a derived
`lidl` output now, so headerContractLidl prefers that over the committed file,
and the hand-kept method list is the pre-migration surface (it still lists
initLogos, which universal modules do not have).
Also records, in the README, that `nix build .#unit-tests` does not configure:
fc39b1b deleted src/capability_module_plugin.{h,cpp} without migrating tests/,
which still compiles them and drives CapabilityModulePlugin. That is this
repo's ONLY check, so it needs porting before this branch merges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test: port the unit tests to the universal shape, and fix the only check
fc39b1b moved this module to `interface: "universal"` and deleted
src/capability_module_plugin.{h,cpp}, but left tests/ compiling them. CMake
failed at generate, and since unit-tests is this repo's ONLY check, the whole
suite has been dark since.
Ported to drive CapabilityModuleImpl directly. No seam was added to the impl
and no production call site changed, which the "grant is per-image, not
per-instance" argument in logos_host_services.h asks for.
The push at the end of requestModule needs no fake: LogosMockSetup (which the
old suite already used) puts the process in LogosMode::Mock, where
MockTransportConnection::requestObject vends a MockLogosObject for any name and
its informModuleToken returns true. Everything else runs for real — the token
registry via lp_token_save, the token_registry gate via lp_token_keys,
lp_client_create, and logos::host::informModuleTokenTo. The only new
requirement is lp_grant_host_services, public C ABI.
Qt types are gone from the test's own surface (std::regex for the UUID shape,
std::set, std::string/std::vector); Qt survives only transitively through the
mock guard.
15 tests ported, 3 added — the grant now has fail-closed coverage the Qt shape
could not express (token_registry ungranted, token_delivery ungranted, an
unreachable target). 18 passed in 2ms.
Mutation-tested, 6 gates, one real build each. Four are load-bearing:
known-caller, known-target, access-policy, and registerRestriction's
trusted-token gate each fail the suite when disabled. Two are NOT, and are
labelled in-file as contract assertions rather than left to look like coverage:
the explicit ungranted() check and the empty-name gate are both shadowed by the
known-caller gate. The empty-TARGET half is shadowed more deeply still — it
survives with three gates removed, only because lp_client_create("") returns
nullptr, which nothing in this repo pins.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): retire the logos-module-builder rev pin, track master
07dba1f pinned logos-module-builder at c60d4a9 (tip of
feat/sdk-codegen-b4-qt-host-repoint) because two things this module cannot
build without were only on that branch. Both are now on module-builder master
(8cd62c7), so the pin is dead weight and the plain url is correct again:
* parsing and validating metadata.json#host_services — lib/parseMetadata.nix,
whose trust-root allowlist names capability_module as the ONLY module
permitted to ask for token_registry/token_delivery; covered by
tests/test-parse-metadata.nix, which uses this module's exact declaration
* emitting the cdylib glue with logos-plugin-qt's logos-qt-host-generator
instead of logos-qt-sdk's stale copy — lib/modulePreConfigure.nix and
lib/buildCppPlugin.nix; asserted by tests/test-module-pre-configure.nix
Checked as FILES on master, not by ancestry: the PRs behind them
(logos-module-builder#203, logos-plugin-qt#19, logos-cpp-sdk#138,
logos-qt-sdk#33, logos-protocol#59) were SQUASH-merged, so
`merge-base --is-ancestor c60d4a9 master` is correctly false and proves nothing.
Relocked with an explicit `nix flake lock --update-input logos-module-builder`
rather than a bare `nix flake lock`, which does not re-resolve an input that is
already locked even once its url stops carrying a rev. Verified the revs moved:
logos-module-builder c60d4a9 -> 8cd62c7 (master)
logos-cpp-sdk a04b278 -> 95d7b3a (master, #138)
logos-qt-sdk 8a06b87 -> 19c844f (master, #33)
logos-plugin-qt cc24fa1 -> 9b2c64e (master, #19)
logos-plugin-core cc24fa1 -> 9b2c64e (same repo, the type:core path this
module actually resolves through)
logos-protocol c8bab12 -> f4407ff (master, #59)
flake.lock shrinks by ~82k lines as the duplicated closures collapse onto one
revision each. No rev-pinned github:logos-co input remains that this repo owns.
VERIFIED on the relocked tree, aarch64-darwin, real builds:
nix build .#default EXIT=0
nix build .#checks.<sys>.unit-tests EXIT=0, 18 passed
lib generate include lidl headers-qt headers-lp
install install-portable lgx all EXIT=0 with an out path
and the two properties the pin existed to guarantee, checked in the OUTPUT:
* generated_code/capability_module_module_impl.cpp exports
logos_module_grant_host_services() -> lp_grant_host_services(), and
capability_module_cdylib_glue.cpp calls it from the host's `hostServices`
property. Both sit behind `LOGOS_PROTOCOL_VERSION_MINOR >= 3`; the closure
links logos-protocol 0.4.0, so the grant path is compiled IN, not out.
* the shipped share/logos/capability_module.lidl is byte-identical to the
DERIVED contract, not to the stale committed src/capability_module.lidl —
confirming the derived contract wins, as docs/ claims.
README's file-tree line said flake.nix "rev-pins logos-module-builder"; updated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): relock onto module-builder master after the C3 grant landed
logos-module-loader-qt#8 merged (acd07cf), so the host-services grant path this
module depends on is on master: module_initializer.cpp stamps the hostServices
property the generated glue reads, and qt_plugin_format_loader's hostServicesFor
supplies it for capability_module.
unit-tests: 18 passed against the relocked closure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
458 lines
19 KiB
C++
458 lines
19 KiB
C++
// Unit tests for CapabilityModuleImpl.
|
|
//
|
|
// capability_module is a universal module: a plain, Qt-free C++ class deriving
|
|
// LogosModuleContext, whose public methods ARE its API (the builder generates
|
|
// the plugin glue). So these tests construct the impl class directly — there is
|
|
// no plugin object and no initLogos step to perform.
|
|
//
|
|
// requestModule() mints a UUID auth token, pushes it to the target module, and
|
|
// returns it to the caller.
|
|
//
|
|
// ── What is REAL here and what is not ────────────────────────────────────────
|
|
//
|
|
// Nothing is faked at the lp_* / host-services layer. Every test drives the
|
|
// genuine article:
|
|
//
|
|
// * the host-services grant — lp_grant_host_services(), the same public C
|
|
// ABI the host calls; the gates inside
|
|
// lp_token_keys / lp_inform_module_token_to
|
|
// really fire
|
|
// * the token registry — lp_token_save() into this image's real
|
|
// TokenManager, read back through the real
|
|
// logos::host::tokenKeys()/tokenFor()
|
|
// * the client — a real lp_client_create()
|
|
// * the token push — a real logos::host::informModuleTokenTo(),
|
|
// which really goes LogosAPIClient ->
|
|
// LogosAPIConsumer::informModuleToken_module
|
|
// -> acquire "<target>__handshake" ->
|
|
// LogosObject::informModuleToken
|
|
//
|
|
// Only the TRANSPORT is substituted, and only by switching the process mode.
|
|
// LogosMockSetup puts the SDK in LogosMode::Mock, where
|
|
// MockTransportConnection::requestObject vends a MockLogosObject for any name
|
|
// and MockLogosObject::informModuleToken returns true. That is what lets the
|
|
// success-path tests run all nine steps of requestModule in-process, with no
|
|
// seam in the impl and no production call site changed — and it is why the
|
|
// tests take ~0 ms rather than timing out on kTokenPushTimeoutMs.
|
|
//
|
|
// The consequence: the mock accepts every push and records nothing, so no test
|
|
// here can assert WHICH module was told or WHICH token it received. Verifying
|
|
// the argument order that capability_module_impl.cpp:127-133 warns about would
|
|
// need a recording endpoint (local mode + a real ModuleProxy), which drags Qt
|
|
// types into this file; the impl is deliberately Qt-free and so is this suite.
|
|
// That gap is unchanged from the pre-migration tests.
|
|
//
|
|
// ── 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
|
|
// this image already holds a token for — the host seeds one entry per loaded
|
|
// module. 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.
|
|
//
|
|
// Under `universal` there is a second, stronger fail-closed precondition the
|
|
// old Qt shape could not express at all: the host-services grant. Reading the
|
|
// token registry now requires "token_registry" and pushing a token requires
|
|
// "token_delivery"; ungranted, requestModule refuses EVERY request. Both are
|
|
// covered below.
|
|
//
|
|
// ── Test isolation ───────────────────────────────────────────────────────────
|
|
//
|
|
// The token store and the host-services grant are both PROCESS-GLOBAL, and
|
|
// lp_grant_host_services REPLACES the grant rather than adding to it, so state
|
|
// leaking between tests would make results order-dependent. CapabilityFixture
|
|
// re-establishes BOTH from scratch on every construction — LogosMockSetup's
|
|
// constructor clears the token store, and the grant is replaced wholesale — so
|
|
// no test can inherit anything from the one before it. Its destructor also
|
|
// clears the grant, so a future test that forgets the fixture fails closed
|
|
// instead of silently borrowing a neighbour's privileges.
|
|
|
|
#include <logos_test.h>
|
|
#include <logos_mock.h> // LogosMockSetup: LogosMode::Mock + token-store reset
|
|
#include <logos_protocol.h> // lp_grant_host_services, lp_token_save, lp_set_mode, LP_OK
|
|
|
|
#include "capability_module_impl.h"
|
|
|
|
#include <cstddef>
|
|
#include <regex>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
// The grant capability_module declares in metadata.json#host_services.
|
|
constexpr const char* kAllHostServices = R"(["token_registry","token_delivery"])";
|
|
// Registry only: enough to verify a caller, NOT enough to deliver the token.
|
|
constexpr const char* kRegistryOnly = R"(["token_registry"])";
|
|
constexpr const char* kNoHostServices = "[]";
|
|
|
|
// One guard per test; construct it FIRST, before any seeding.
|
|
//
|
|
// Order matters and is enforced by declaration order: m_mock is built before
|
|
// the grant, and LogosMockSetup's constructor calls clearAllTokens(). Seeding
|
|
// before the fixture would therefore be silently wiped.
|
|
class CapabilityFixture {
|
|
public:
|
|
explicit CapabilityFixture(const char* servicesJson = kAllHostServices)
|
|
: m_grantRc(lp_grant_host_services(servicesJson)) {}
|
|
|
|
~CapabilityFixture() { lp_grant_host_services(kNoHostServices); }
|
|
|
|
CapabilityFixture(const CapabilityFixture&) = delete;
|
|
CapabilityFixture& operator=(const CapabilityFixture&) = delete;
|
|
|
|
int grantRc() const { return m_grantRc; }
|
|
|
|
private:
|
|
LogosMockSetup m_mock; // must be declared first: it clears the token store
|
|
int m_grantRc;
|
|
};
|
|
|
|
// Seed a module's token so capability_module treats it as a known/loaded
|
|
// module — the test-side stand-in for the host seeding one entry per module it
|
|
// loads. Goes through the real C ABI into the real image token store.
|
|
void seedModule(const std::string& name) {
|
|
lp_token_save(name.c_str(), ("seed-token-" + name).c_str());
|
|
}
|
|
|
|
// 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 std::string kTrustedToken = "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");
|
|
}
|
|
|
|
// UUID without braces: 8-4-4-4-12 lowercase hex digits separated by hyphens.
|
|
bool isUuid(const std::string& s) {
|
|
static const std::regex re(
|
|
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
|
|
return std::regex_match(s, re);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ── Success path: both caller and target are known modules ──────────────────
|
|
|
|
LOGOS_TEST(requestModule_returns_uuid_format_token) {
|
|
CapabilityFixture fixture;
|
|
LOGOS_ASSERT_EQ(fixture.grantRc(), LP_OK);
|
|
seedModule("requester_module");
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("requester_module", "target_module");
|
|
|
|
LOGOS_ASSERT_FALSE(token.empty());
|
|
LOGOS_ASSERT(isUuid(token));
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_mints_unique_token_per_call) {
|
|
CapabilityFixture fixture;
|
|
seedModule("requester");
|
|
seedModule("target");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
std::set<std::string> tokens;
|
|
for (int i = 0; i < 10; ++i) {
|
|
tokens.insert(impl.requestModule("requester", "target"));
|
|
}
|
|
|
|
// Also the sharpest liveness detector in the suite: if the push had failed,
|
|
// all ten would be "" and the set would collapse to size 1.
|
|
LOGOS_ASSERT_EQ(tokens.size(), std::size_t(10));
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_works_when_target_token_is_pre_seeded) {
|
|
CapabilityFixture fixture;
|
|
// Seed both the caller and the target — exercises the tokenFor() path for
|
|
// the target while satisfying the known-caller gate. The literal value
|
|
// differs from seedModule's to show nothing reads it.
|
|
seedModule("requester_module");
|
|
lp_token_save("target_module", "pre-seeded-token");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("requester_module", "target_module");
|
|
|
|
LOGOS_ASSERT(isUuid(token));
|
|
}
|
|
|
|
// ── F-001 security regression: fail closed on unverified input ──────────────
|
|
|
|
// Replaces the old `requestModule_returns_empty_when_not_initialized`. The
|
|
// universal shape has no init step and no injected LogosAPI, so there is no
|
|
// "not initialized" state to test; the nearest real fail-closed precondition —
|
|
// and a stronger one — is the host-services grant this module now depends on.
|
|
//
|
|
// Note this asserts the CONTRACT, not one code path: with no grant,
|
|
// tokenKeys() also comes back empty, so the known-caller gate would refuse
|
|
// these inputs even if the explicit ungranted() check were deleted.
|
|
LOGOS_TEST(requestModule_returns_empty_when_host_services_ungranted) {
|
|
CapabilityFixture fixture(kNoHostServices);
|
|
LOGOS_ASSERT_EQ(fixture.grantRc(), LP_OK);
|
|
seedModule("requester");
|
|
seedModule("target");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
// Both names are known and the pair is unrestricted: the ONLY thing
|
|
// refusing this request is the missing grant.
|
|
const std::string token = impl.requestModule("requester", "target");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
// The second half of the grant, which nothing else covers: an image allowed to
|
|
// VERIFY a caller but not to DELIVER a token must still refuse, rather than
|
|
// hand back a token the target was never told about.
|
|
LOGOS_TEST(requestModule_returns_empty_when_token_delivery_ungranted) {
|
|
CapabilityFixture fixture(kRegistryOnly);
|
|
LOGOS_ASSERT_EQ(fixture.grantRc(), LP_OK);
|
|
seedModule("requester_module");
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
// Clears gates 1-5 and mints a token; the push then comes back
|
|
// LP_ERR_UNSUPPORTED, so the minted token is dropped on the floor.
|
|
const std::string token = impl.requestModule("requester_module", "target_module");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
// These two also assert the CONTRACT rather than one code path, and neither is
|
|
// currently falsifiable by this suite (verified by mutation: disabling the
|
|
// empty-name gate leaves all 18 green).
|
|
//
|
|
// caller half — shadowed by the known-caller gate: "" is never a key in
|
|
// tokenKeys(), so the request is refused one gate later either way. Remove
|
|
// BOTH gates and this test does fail, so the assertion is real.
|
|
// target half — shadowed twice over, and the deeper shadow is not ours:
|
|
// it survives even with the empty-name, known-caller and known-target gates
|
|
// all removed, because lp_client_create("", ...) happens to return nullptr.
|
|
// That is logos-protocol behaviour with nothing in this repo pinning it. If
|
|
// it ever accepted an empty target AND the gate below were dropped,
|
|
// requestModule would mint a token for an empty target and nothing here
|
|
// would notice.
|
|
LOGOS_TEST(requestModule_rejects_empty_fromModuleName) {
|
|
CapabilityFixture fixture;
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("", "target_module");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_rejects_empty_targetModuleName) {
|
|
CapabilityFixture fixture;
|
|
seedModule("requester_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("requester_module", "");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_rejects_unknown_fromModuleName) {
|
|
CapabilityFixture fixture;
|
|
// Only the target is known; the requesting identity was never loaded.
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
// Spoofing a non-loaded identity must not mint a token.
|
|
const std::string token = impl.requestModule("spoofed_module", "target_module");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_rejects_unknown_target) {
|
|
CapabilityFixture fixture;
|
|
// Only the caller is known; the target was never loaded.
|
|
seedModule("requester_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("requester_module", "missing_target");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_succeeds_for_known_caller_and_target) {
|
|
// The positive control for this section. Without it every assertion above
|
|
// would still pass against a requestModule that returned "" unconditionally
|
|
// — or against a harness that had quietly stopped working.
|
|
CapabilityFixture fixture;
|
|
seedModule("requester_module");
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("requester_module", "target_module");
|
|
|
|
LOGOS_ASSERT_FALSE(token.empty());
|
|
LOGOS_ASSERT(isUuid(token));
|
|
}
|
|
|
|
// The remaining refusal in requestModule: the push reached a real transport and
|
|
// genuinely failed, as opposed to being refused for want of a grant. Mock mode
|
|
// cannot produce it — MockLogosObject accepts everything — so this one test
|
|
// runs in LogosMode::Local with nothing published in the PluginRegistry.
|
|
// LocalTransportConnection::requestObject then misses on both the handshake
|
|
// surface and the business object and returns immediately, so this costs no
|
|
// wall-clock time despite exercising the failure arm.
|
|
LOGOS_TEST(requestModule_returns_empty_when_target_is_unreachable) {
|
|
CapabilityFixture fixture;
|
|
LOGOS_ASSERT_EQ(lp_set_mode("local"), LP_OK); // fixture's dtor restores the mode
|
|
seedModule("requester_module");
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("requester_module", "target_module");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
// ── 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) {
|
|
CapabilityFixture fixture;
|
|
seedTrustedChannel();
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
// Passes the trusted-token gate, then is refused by the empty-target gate —
|
|
// which pins the gate ORDER: trust is checked before argument validity.
|
|
LOGOS_ASSERT_FALSE(impl.registerRestriction(kTrustedToken, "", {"caller"}));
|
|
}
|
|
|
|
LOGOS_TEST(registerRestriction_rejects_untrusted_caller_token) {
|
|
// A loaded module can reach this method (the generic authorization that
|
|
// fronts it 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.
|
|
CapabilityFixture fixture;
|
|
seedTrustedChannel();
|
|
seedModule("malicious_module");
|
|
seedModule("package_manager");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
// malicious_module tries to grant itself access using its OWN token.
|
|
const bool ok = impl.registerRestriction(
|
|
"seed-token-malicious_module", "package_manager", {"malicious_module"});
|
|
LOGOS_ASSERT_FALSE(ok);
|
|
|
|
// And an empty token is rejected too.
|
|
LOGOS_ASSERT_FALSE(impl.registerRestriction(
|
|
"", "package_manager", {"malicious_module"}));
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_allows_listed_caller_for_restricted_target) {
|
|
CapabilityFixture fixture;
|
|
seedTrustedChannel();
|
|
seedModule("package_manager_ui");
|
|
seedModule("package_manager");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
LOGOS_ASSERT_TRUE(impl.registerRestriction(
|
|
kTrustedToken, "package_manager", {"package_manager_ui"}));
|
|
|
|
const std::string token = impl.requestModule("package_manager_ui", "package_manager");
|
|
|
|
LOGOS_ASSERT_FALSE(token.empty());
|
|
LOGOS_ASSERT(isUuid(token));
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_denies_unlisted_caller_for_restricted_target) {
|
|
CapabilityFixture fixture;
|
|
seedTrustedChannel();
|
|
seedModule("some_other_module");
|
|
seedModule("package_manager");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
impl.registerRestriction(kTrustedToken, "package_manager", {"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.
|
|
const std::string token = impl.requestModule("some_other_module", "package_manager");
|
|
|
|
LOGOS_ASSERT_TRUE(token.empty());
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_allows_any_caller_for_unrestricted_target) {
|
|
// Pins the deliberate fail-OPEN policy decision (see the
|
|
// TODO(access-policy) in capability_module_impl.cpp). Nothing else does:
|
|
// flipping to deny-by-default must turn THIS test red.
|
|
CapabilityFixture fixture;
|
|
seedTrustedChannel();
|
|
seedModule("some_module");
|
|
seedModule("restricted_target");
|
|
seedModule("open_target");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
// Restrict only restricted_target; open_target has no restriction.
|
|
impl.registerRestriction(kTrustedToken, "restricted_target", {"allowed_caller"});
|
|
|
|
const std::string token = impl.requestModule("some_module", "open_target");
|
|
|
|
LOGOS_ASSERT_FALSE(token.empty());
|
|
LOGOS_ASSERT(isUuid(token));
|
|
}
|
|
|
|
LOGOS_TEST(requestModule_allows_all_when_no_restriction_registered) {
|
|
// Back-compat: with no policy pushed, every known caller/target pair works.
|
|
CapabilityFixture fixture;
|
|
seedModule("requester_module");
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
const std::string token = impl.requestModule("requester_module", "target_module");
|
|
|
|
LOGOS_ASSERT_FALSE(token.empty());
|
|
}
|
|
|
|
LOGOS_TEST(registerRestriction_overwrites_previous_for_same_target) {
|
|
CapabilityFixture fixture;
|
|
seedTrustedChannel();
|
|
seedModule("old_caller");
|
|
seedModule("new_caller");
|
|
seedModule("target_module");
|
|
|
|
CapabilityModuleImpl impl;
|
|
|
|
impl.registerRestriction(kTrustedToken, "target_module", {"old_caller"});
|
|
// Re-register (as core does each boot) with a different allowed set.
|
|
impl.registerRestriction(kTrustedToken, "target_module", {"new_caller"});
|
|
|
|
// old_caller is no longer allowed; new_caller is. The second assertion is
|
|
// what distinguishes "overwritten" from "registerRestriction broke the
|
|
// target for everyone".
|
|
LOGOS_ASSERT_TRUE(impl.requestModule("old_caller", "target_module").empty());
|
|
LOGOS_ASSERT_FALSE(impl.requestModule("new_caller", "target_module").empty());
|
|
}
|