support setting access policy (#145)

This commit is contained in:
Iuri Matias
2026-06-10 18:17:54 -04:00
committed by GitHub
parent 68e408a5de
commit 098fed4527
14 changed files with 393 additions and 70 deletions
+3 -2
View File
@@ -121,8 +121,9 @@ void logos_core_set_persistence_base_path(const char* path);
void logos_core_set_module_transports(const char* name, const char* transport_set_json);
// Inter-module access policy (per-target allowed-caller allowlists).
// Should be called before the modules are loaded. NULL/"" clears it.
// NOTE: currently a no-op — accepted but not yet enforced (TODO).
// Core parses it and registers the per-target restrictions with
// capability_module, which then denies token issuance for disallowed
// (caller, target) pairs. Call before logos_core_start(); NULL/"" clears.
void logos_core_set_access_policy(const char* policy_json);
// Module management
+1 -1
View File
@@ -313,7 +313,7 @@ The public C API (`logos_core.h`) is the only exported interface. All functions
| `logos_core_add_modules_dir(dir)` | Add a module directory to scan (duplicates ignored) |
| `logos_core_set_persistence_base_path(path)` | Set base directory for module instance persistence |
| `logos_core_set_module_transports(name, json)` | Register a per-module transport set (JSON, see logos-cpp-sdk shape). Forwarded to the child via `--transport-set` so its `LogosAPIProvider` binds every listener instead of only the global default LocalSocket. Must be called before the module is loaded; empty clears the entry |
| `logos_core_set_access_policy(json)` | Install the inter-module access policy (version + mode + per-target `allowedCallers` allowlists). Should be called before modules are loaded; NULL/empty clears it. **Currently a no-op** — accepted but not yet enforced (TODO) |
| `logos_core_set_access_policy(json)` | Install the inter-module access policy (version + mode + per-target `allowedCallers` allowlists). Core parses it and registers the per-target restrictions with capability_module, which denies token issuance (and thus calls) for disallowed callers when `mode` is `enforce`. Call before modules load; NULL/empty clears it |
| `logos_core_load_module(name, with_dependencies) → int` | Load a module (1 = success, 0 = failure). When `with_dependencies` is true, resolves the dependency tree and loads in topological order |
| `logos_core_unload_module(name, with_dependents) → int` | Unload a module. When `with_dependents` is true, cascade unloads every loaded transitive dependent leaves-first. Returns 1 only if every step succeeded |
| `logos_core_get_module_dependencies(name, recursive) → char**` | Modules that `name` depends on (forward edges). `recursive=true` walks the forward graph transitively. Unknown names yield an empty array. Caller frees |
+1 -1
View File
@@ -245,7 +245,7 @@ The platform supports two build variants:
| `logos_core_get_module_dependents(name, recursive) → char**` | Return null-terminated array of modules that depend on `name` (reverse edges). With `recursive=true`, walks the reverse dependency graph transitively via BFS. Unknown names yield an empty array. Caller must free. |
| `logos_core_process_module(path) → char*` | Read a module file's metadata and register it as known without loading. Returns the module name or NULL. Caller must free. |
| `logos_core_set_module_transports(name, json)` | Register a per-module `LogosTransportSet` (JSON, see logos-cpp-sdk shape) for the named module. The runtime forwards it to the child via `--transport-set` so the child's `LogosAPIProvider` binds every transport instead of only the global default LocalSocket. Must be called before the module is loaded. NULL or empty clears any previously-registered entry. |
| `logos_core_set_access_policy(json)` | Install the inter-module access policy: a JSON document with `version`, `mode` (e.g. `enforce`), and `restrictions` mapping each target module to its `allowedCallers` allowlist. Should be called before modules are loaded. NULL or empty clears any previously-set policy. **Currently a no-op** — the policy is accepted but not yet enforced (TODO). |
| `logos_core_set_access_policy(json)` | Install the inter-module access policy: a JSON document with `version`, `mode` (e.g. `enforce`), and `restrictions` mapping each target module to its `allowedCallers` allowlist. Core parses it and, once capability_module loads, registers the concrete per-target restrictions with it via `registerRestriction` (authenticated by capability_module's auth token, so only the trusted core channel can register or relax restrictions — a peer module cannot); capability_module then refuses to mint a token (in `requestModule`) for a caller not in a restricted target's allowlist, so the call can never proceed. Only `mode: "enforce"` activates gating. Call before modules load. NULL or empty clears any previously-set policy. |
### Token and Monitoring
Generated
+3 -3
View File
@@ -5,11 +5,11 @@
"logos-module-builder": "logos-module-builder"
},
"locked": {
"lastModified": 1778182598,
"narHash": "sha256-QoyirL/blmVM2cmXQwbR6rmHufQJ6vaJSCkNORu4qaA=",
"lastModified": 1781129198,
"narHash": "sha256-vnJCkDQlGKXStnXFGhATzY3dXwwQT4/nnljReFmAzQc=",
"owner": "logos-co",
"repo": "logos-capability-module",
"rev": "e675e9e3a98ee69bb303365c2c626f9237bc1ab5",
"rev": "5d438961e2defe72b76028150787d59e586eec8c",
"type": "github"
},
"original": {
+2
View File
@@ -89,6 +89,8 @@ set(LOGOS_CORE_SOURCES
logos_core/module_registry.h
logos_core/dependency_resolver.cpp
logos_core/dependency_resolver.h
logos_core/access_policy.cpp
logos_core/access_policy.h
logos_core/module_manager.cpp
logos_core/module_manager.h
logos_core/module_runtime.h
+52
View File
@@ -0,0 +1,52 @@
#include "access_policy.h"
#include <nlohmann/json.hpp>
namespace LogosCore {
std::optional<AccessPolicy> parseAccessPolicy(const std::string& json)
{
nlohmann::json doc;
try {
doc = nlohmann::json::parse(json);
} catch (const std::exception&) {
return std::nullopt; // invalid JSON is the only hard failure
}
if (!doc.is_object())
return std::nullopt;
AccessPolicy policy;
policy.version = doc.value("version", 0);
policy.mode = doc.value("mode", std::string{});
auto restrictionsIt = doc.find("restrictions");
if (restrictionsIt != doc.end() && restrictionsIt->is_object()) {
for (auto it = restrictionsIt->begin(); it != restrictionsIt->end(); ++it) {
const std::string& target = it.key();
if (target.empty())
continue;
AccessRestriction restriction;
restriction.target = target;
const auto& entry = it.value();
if (entry.is_object()) {
auto callersIt = entry.find("allowedCallers");
if (callersIt != entry.end() && callersIt->is_array()) {
for (const auto& caller : *callersIt) {
if (caller.is_string())
restriction.allowedCallers.push_back(
caller.get<std::string>());
}
}
}
policy.restrictions.push_back(std::move(restriction));
}
}
return policy;
}
} // namespace LogosCore
+37
View File
@@ -0,0 +1,37 @@
#ifndef ACCESS_POLICY_H
#define ACCESS_POLICY_H
#include <optional>
#include <string>
#include <vector>
// Inter-module access policy model + parser (Qt-free). Parses the JSON set
// via logos_core_set_access_policy(), e.g.
// {"version":1,"mode":"enforce","restrictions":{
// "package_manager":{"allowedCallers":["package_manager_ui"]}}}
namespace LogosCore {
// One target module and the set of caller modules permitted to reach it.
struct AccessRestriction {
std::string target;
std::vector<std::string> allowedCallers;
};
struct AccessPolicy {
int version = 0;
std::string mode;
std::vector<AccessRestriction> restrictions;
// Only "enforce" turns restrictions into denials; any other value
// leaves the policy informational and core registers nothing.
bool enforce() const { return mode == "enforce"; }
};
// Returns nullopt only on invalid JSON. Otherwise tolerant: unknown keys
// ignored, missing "restrictions"/"allowedCallers" yield empty lists.
std::optional<AccessPolicy> parseAccessPolicy(const std::string& json);
} // namespace LogosCore
#endif // ACCESS_POLICY_H
+4 -10
View File
@@ -103,16 +103,10 @@ void logos_core_set_module_transports(const char* module_name,
}
void logos_core_set_access_policy(const char* policy_json) {
// No-op for now: the policy is accepted but not yet enforced. A NULL
// or empty argument is the documented "clear the policy" signal, so
// it's explicitly allowed (unlike the module-name setters above, this
// does not abort on NULL).
//
// TODO: Parse `policy_json` (version / mode / per-target
// allowedCallers) and enforce the per-target allowed-caller checks
// on the inter-module call path. Until then, every call is permitted
// regardless of the policy supplied here.
(void)policy_json;
// NULL/"" clears the policy (see header) — unlike the module-name
// setters above, this does not abort on NULL.
ModuleManager::setAccessPolicy(
policy_json ? std::string(policy_json) : std::string{});
}
void logos_core_refresh_modules()
+8 -15
View File
@@ -105,10 +105,8 @@ LOGOS_CORE_EXPORT void logos_core_set_persistence_base_path(const char* path);
LOGOS_CORE_EXPORT void logos_core_set_module_transports(const char* module_name,
const char* transport_set_json);
// Install the inter-module access policy that governs which caller
// modules may invoke which target modules.
//
// `policy_json` is a JSON document of the shape:
// Install the inter-module access policy: which callers may invoke which
// targets. `policy_json` shape:
//
// {
// "version": 1,
@@ -119,18 +117,13 @@ LOGOS_CORE_EXPORT void logos_core_set_module_transports(const char* module_name,
// }
// }
//
// `mode` selects how violations are handled (e.g. "enforce" to deny,
// other modes may log only). Each entry under `restrictions` names a
// target module and the set of caller modules permitted to reach it;
// a target absent from `restrictions` is unrestricted.
// A restricted target rejects callers outside its allowlist; a target
// absent from `restrictions` is unrestricted. Only `mode` == "enforce"
// activates gating (any other value registers nothing). Enforced by
// capability_module, which won't issue a token — hence won't allow the
// call — for a disallowed caller.
//
// Should be called before logos_core_start() so the policy is in place
// before any module is loaded. NULL or "" clears any previously-set
// policy.
//
// TODO: This is currently a no-op — the policy is accepted but not yet
// enforced. Implement parsing of the JSON document and wire the
// per-target allowed-caller checks into the inter-module call path.
// Must be called before logos_core_start(). NULL or "" clears the policy.
LOGOS_CORE_EXPORT void logos_core_set_access_policy(const char* policy_json);
// Re-scan all module directories and update known modules.
+94 -30
View File
@@ -1,5 +1,6 @@
#include "module_manager.h"
#include "module_registry.h"
#include "access_policy.h"
#include "dependency_resolver.h"
#include "runtime_registry.h"
#include "composite_runtime.h"
@@ -18,6 +19,9 @@
#include "logos_transport_config_json.h"
#include "token_manager.h"
#include "instance_persistence.h"
#include <QString>
#include <QStringList>
#include <QVariant>
namespace {
ModuleRegistry& registryInstance() {
@@ -44,6 +48,13 @@ namespace {
return path;
}
// Raw access-policy JSON, set before logos_core_start(); pushed to
// capability_module once it loads. Guarded by loadMutex().
std::string& accessPolicyJson() {
static std::string s;
return s;
}
LogosCore::RuntimeRegistry& runtimeRegistry() {
static LogosCore::RuntimeRegistry reg;
static std::once_flag initFlag;
@@ -72,6 +83,73 @@ namespace {
return result;
}
// Dial capability_module from a long-lived "core" LogosAPI. Prefer the
// operator's first configured transport; fall back to the global
// default (LocalSocket). Needed because the single-arg getClient()
// always uses the global default, which hangs against a tcp-only
// capability_module that never bound a LocalSocket.
LogosAPIClient* capabilityModuleClient() {
static LogosAPI* s_coreApi = nullptr;
if (!s_coreApi)
s_coreApi = new LogosAPI(std::string("core"));
if (auto it = moduleTransportsMap().find("capability_module");
it != moduleTransportsMap().end() && !it->second.empty()) {
const auto ts = logos::transportSetFromJsonString(it->second);
if (!ts.empty()) {
return s_coreApi->getClient(
QStringLiteral("capability_module"), ts.front());
}
}
return s_coreApi->getClient(std::string("capability_module"));
}
// Parse the stored access policy and register its per-target restrictions
// with capability_module. Only "enforce" mode registers anything; an
// empty/unparseable policy leaves it unrestricted. Best-effort (failures
// logged, not fatal).
void pushAccessRestrictionsToCapabilityModule() {
if (accessPolicyJson().empty())
return;
if (!registryInstance().isLoaded("capability_module"))
return;
auto policy = LogosCore::parseAccessPolicy(accessPolicyJson());
if (!policy) {
spdlog::warn("Access policy is not valid JSON — not enforcing any restrictions");
return;
}
if (!policy->enforce())
return;
const std::string capabilityModuleToken =
TokenManager::instance().getToken(std::string("capability_module"));
LogosAPIClient* client = capabilityModuleClient();
for (const auto& restriction : policy->restrictions) {
QStringList allowedCallers;
for (const auto& caller : restriction.allowedCallers)
allowedCallers.append(QString::fromStdString(caller));
// The token is the trusted-channel proof registerRestriction
// verifies — only core holds it.
const QVariant result = client->invokeRemoteMethod(
QStringLiteral("capability_module"),
QStringLiteral("registerRestriction"),
QVariant(QString::fromStdString(capabilityModuleToken)),
QVariant(QString::fromStdString(restriction.target)),
QVariant(allowedCallers));
if (!result.toBool()) {
spdlog::warn("Failed to register access restriction for target: {}",
restriction.target);
} else {
spdlog::info("Registered access restriction for target: {} ({} allowed callers)",
restriction.target, restriction.allowedCallers.size());
}
}
}
void notifyCapabilityModule(const std::string& name, const std::string& token) {
if (!registryInstance().isLoaded("capability_module"))
return;
@@ -79,36 +157,7 @@ namespace {
TokenManager& tokenManager = TokenManager::instance();
std::string capabilityModuleToken = tokenManager.getToken(std::string("capability_module"));
static LogosAPI* s_coreApi = nullptr;
if (!s_coreApi)
s_coreApi = new LogosAPI(std::string("core"));
// Pick the right transport for dialing capability_module. The
// single-arg getClient() defaults to the *global* transport
// config (LocalSocket), which is wrong when an operator
// configured capability_module for tcp / tcp_ssl only — the
// parent's RPC then hangs trying to reach a LocalSocket that
// capability_module never bound.
//
// Resolution order matches the operator's intent: prefer the
// first transport the operator named (so a `--module-transport
// capability_module=local --module-transport
// capability_module=tcp` setup uses LocalSocket, but a tcp-only
// setup uses tcp). Empty / unset map → fall back to the global
// default, which is correct for the default LocalSocket-only
// case (no per-module config registered).
LogosAPIClient* client = nullptr;
if (auto it = moduleTransportsMap().find("capability_module");
it != moduleTransportsMap().end() && !it->second.empty()) {
const auto ts = logos::transportSetFromJsonString(it->second);
if (!ts.empty()) {
client = s_coreApi->getClient(
QStringLiteral("capability_module"), ts.front());
}
}
if (!client) {
client = s_coreApi->getClient(std::string("capability_module"));
}
LogosAPIClient* client = capabilityModuleClient();
if (!client->informModuleToken(capabilityModuleToken, name, token)) {
spdlog::warn("Failed to register token with capability module for: {}", name);
@@ -264,6 +313,17 @@ namespace ModuleManager {
moduleTransportsMap()[moduleName] = transportSetJson;
}
void setAccessPolicy(const std::string& policyJson) {
std::lock_guard<std::mutex> g(loadMutex()); // guards the read at push time
accessPolicyJson() = policyJson;
// Validate eagerly so a malformed policy is flagged at set time, not
// silently at capability_module load. Stored verbatim regardless.
if (!policyJson.empty() && !LogosCore::parseAccessPolicy(policyJson)) {
spdlog::warn("logos_core_set_access_policy: policy is not valid JSON "
"— no restrictions will be enforced");
}
}
void discoverInstalledModules() {
registryInstance().discoverInstalledModules();
}
@@ -346,6 +406,9 @@ namespace ModuleManager {
return false;
}
// Register restrictions now, before any other module can call out.
pushAccessRestrictionsToCapabilityModule();
return true;
}
@@ -435,6 +498,7 @@ namespace ModuleManager {
// clear() between scenarios) would inherit the previous
// run's transport map and bind unexpected ports.
moduleTransportsMap().clear();
accessPolicyJson().clear(); // same rationale — don't leak across restarts
}
char** getLoadedModulesCStr() {
+7
View File
@@ -35,6 +35,13 @@ namespace ModuleManager {
void setModuleTransports(const std::string& moduleName,
const std::string& transportSetJson);
// Store the inter-module access policy (the raw JSON document set via
// logos_core_set_access_policy). Core parses it and registers the
// concrete per-target restrictions with capability_module once that
// module is loaded (see initializeCapabilityModule). Must be called
// BEFORE logos_core_start(). Empty clears any previously set policy.
void setAccessPolicy(const std::string& policyJson);
void discoverInstalledModules();
std::string processModule(const std::string& modulePath);
+1
View File
@@ -9,6 +9,7 @@ get_filename_component(LOGOS_CPP_SDK_ROOT "${LOGOS_CPP_SDK_ROOT}" ABSOLUTE)
add_executable(logos_core_tests
test_app_lifecycle.cpp
test_access_policy.cpp
test_module_manager.cpp
test_process_stats.cpp
test_dependency_resolver.cpp
+171
View File
@@ -0,0 +1,171 @@
// =============================================================================
// Tests for the access-policy parser (src/logos_core/access_policy.{h,cpp}).
//
// parseAccessPolicy turns the JSON document set via
// logos_core_set_access_policy into a structured AccessPolicy. Core uses it to
// register concrete per-target restrictions with capability_module; enforcement
// itself lives in capability_module (tested there). These tests pin the parse
// contract:
// - the exact basecamp/daemon production document parses correctly
// - mode != "enforce" is reflected via enforce()==false (core registers nothing)
// - tolerant parsing: unknown keys ignored, missing/!object restrictions ->
// empty, missing allowedCallers -> target with empty caller list
// - the ONLY hard failure is invalid JSON -> std::nullopt
// =============================================================================
#include <gtest/gtest.h>
#include "access_policy.h"
#include <algorithm>
#include <string>
using LogosCore::AccessPolicy;
using LogosCore::parseAccessPolicy;
namespace {
// Find a restriction by target name in a parsed policy, or nullptr.
const LogosCore::AccessRestriction* findTarget(const AccessPolicy& p,
const std::string& target) {
for (const auto& r : p.restrictions)
if (r.target == target) return &r;
return nullptr;
}
bool callersContain(const LogosCore::AccessRestriction& r, const std::string& caller) {
return std::find(r.allowedCallers.begin(), r.allowedCallers.end(), caller)
!= r.allowedCallers.end();
}
// The exact document basecamp (app/main.cpp) and the logoscore daemon pass.
const char* kProductionPolicy =
"{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{"
"\"package_manager\":{\"allowedCallers\":[\"package_manager_ui\"]},"
"\"package_downloader\":{\"allowedCallers\":[\"package_manager_ui\"]}}}";
} // namespace
// ── Production document ──────────────────────────────────────────────────────
TEST(AccessPolicyParse, ParsesProductionDocument) {
auto policy = parseAccessPolicy(kProductionPolicy);
ASSERT_TRUE(policy.has_value());
EXPECT_EQ(policy->version, 1);
EXPECT_EQ(policy->mode, "enforce");
EXPECT_TRUE(policy->enforce());
ASSERT_EQ(policy->restrictions.size(), 2u);
const auto* pm = findTarget(*policy, "package_manager");
ASSERT_NE(pm, nullptr);
ASSERT_EQ(pm->allowedCallers.size(), 1u);
EXPECT_TRUE(callersContain(*pm, "package_manager_ui"));
const auto* pd = findTarget(*policy, "package_downloader");
ASSERT_NE(pd, nullptr);
EXPECT_TRUE(callersContain(*pd, "package_manager_ui"));
}
// ── mode semantics ───────────────────────────────────────────────────────────
TEST(AccessPolicyParse, NonEnforceModeReportsEnforceFalse) {
auto policy = parseAccessPolicy(
"{\"version\":1,\"mode\":\"audit\",\"restrictions\":{"
"\"package_manager\":{\"allowedCallers\":[\"package_manager_ui\"]}}}");
ASSERT_TRUE(policy.has_value());
EXPECT_EQ(policy->mode, "audit");
EXPECT_FALSE(policy->enforce());
// Restrictions still parse — core just won't register them in non-enforce.
EXPECT_EQ(policy->restrictions.size(), 1u);
}
TEST(AccessPolicyParse, MissingModeIsNotEnforce) {
auto policy = parseAccessPolicy("{\"version\":1,\"restrictions\":{}}");
ASSERT_TRUE(policy.has_value());
EXPECT_FALSE(policy->enforce());
}
// ── Multiple callers per target ──────────────────────────────────────────────
TEST(AccessPolicyParse, ParsesMultipleAllowedCallers) {
auto policy = parseAccessPolicy(
"{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{"
"\"target\":{\"allowedCallers\":[\"a\",\"b\",\"c\"]}}}");
ASSERT_TRUE(policy.has_value());
const auto* t = findTarget(*policy, "target");
ASSERT_NE(t, nullptr);
EXPECT_EQ(t->allowedCallers.size(), 3u);
EXPECT_TRUE(callersContain(*t, "a"));
EXPECT_TRUE(callersContain(*t, "b"));
EXPECT_TRUE(callersContain(*t, "c"));
}
// ── Tolerant parsing ─────────────────────────────────────────────────────────
TEST(AccessPolicyParse, EmptyRestrictionsObjectYieldsNoRestrictions) {
auto policy = parseAccessPolicy("{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{}}");
ASSERT_TRUE(policy.has_value());
EXPECT_TRUE(policy->enforce());
EXPECT_TRUE(policy->restrictions.empty());
}
TEST(AccessPolicyParse, MissingRestrictionsKeyYieldsNoRestrictions) {
auto policy = parseAccessPolicy("{\"version\":1,\"mode\":\"enforce\"}");
ASSERT_TRUE(policy.has_value());
EXPECT_TRUE(policy->restrictions.empty());
}
TEST(AccessPolicyParse, TargetWithoutAllowedCallersYieldsEmptyCallerList) {
// A restricted target with no allowedCallers means "nobody may call it"
// once enforced — the parser surfaces it as a target with zero callers.
auto policy = parseAccessPolicy(
"{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{\"target\":{}}}");
ASSERT_TRUE(policy.has_value());
const auto* t = findTarget(*policy, "target");
ASSERT_NE(t, nullptr);
EXPECT_TRUE(t->allowedCallers.empty());
}
TEST(AccessPolicyParse, UnknownTopLevelKeysAreIgnored) {
auto policy = parseAccessPolicy(
"{\"version\":1,\"mode\":\"enforce\",\"futureField\":42,"
"\"restrictions\":{\"target\":{\"allowedCallers\":[\"a\"],\"note\":\"x\"}}}");
ASSERT_TRUE(policy.has_value());
EXPECT_TRUE(policy->enforce());
const auto* t = findTarget(*policy, "target");
ASSERT_NE(t, nullptr);
EXPECT_EQ(t->allowedCallers.size(), 1u);
}
TEST(AccessPolicyParse, NonStringCallersAreSkipped) {
auto policy = parseAccessPolicy(
"{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{"
"\"target\":{\"allowedCallers\":[\"ok\",123,null,\"also_ok\"]}}}");
ASSERT_TRUE(policy.has_value());
const auto* t = findTarget(*policy, "target");
ASSERT_NE(t, nullptr);
EXPECT_EQ(t->allowedCallers.size(), 2u);
EXPECT_TRUE(callersContain(*t, "ok"));
EXPECT_TRUE(callersContain(*t, "also_ok"));
}
// ── Hard failures ────────────────────────────────────────────────────────────
TEST(AccessPolicyParse, InvalidJsonReturnsNullopt) {
EXPECT_FALSE(parseAccessPolicy("{not valid json").has_value());
EXPECT_FALSE(parseAccessPolicy("").has_value());
EXPECT_FALSE(parseAccessPolicy("garbage").has_value());
}
TEST(AccessPolicyParse, NonObjectJsonReturnsNullopt) {
// Valid JSON, but not a policy object.
EXPECT_FALSE(parseAccessPolicy("[1,2,3]").has_value());
EXPECT_FALSE(parseAccessPolicy("\"a string\"").has_value());
EXPECT_FALSE(parseAccessPolicy("42").has_value());
}
TEST(AccessPolicyParse, VersionDefaultsToZeroWhenAbsent) {
auto policy = parseAccessPolicy("{\"mode\":\"enforce\",\"restrictions\":{}}");
ASSERT_TRUE(policy.has_value());
EXPECT_EQ(policy->version, 0);
}
+9 -8
View File
@@ -120,14 +120,15 @@ TEST_F(AppLifecycleTest, Start_UsesCustomModulesDirs) {
// Access Policy Tests
// =============================================================================
//
// logos_core_set_access_policy is currently a no-op: the policy is
// accepted but not yet enforced (see the TODO in logos_core.cpp). These
// tests don't assert any behavioural effect — there's nothing to observe
// yet — but they pin the contract that already holds: the call accepts a
// well-formed policy, an empty string, and NULL (the documented "clear"
// signal) without crashing or aborting, and without disturbing unrelated
// core state. When enforcement lands, these become the scaffold for
// asserting the policy actually gates calls.
// logos_core_set_access_policy stores the policy; core parses it and
// registers the concrete per-target restrictions with capability_module
// once that module loads (inside logos_core_start). The parser is unit-
// tested in test_access_policy.cpp, and the enforcement (deny token
// issuance for a disallowed caller) is tested in capability_module's own
// suite. Here we only pin the C-API setter contract that holds without a
// running capability_module: the call accepts a well-formed policy, an
// empty string, and NULL (the documented "clear" signal) without crashing
// or aborting, and without disturbing unrelated core state.
TEST_F(AppLifecycleTest, SetAccessPolicy_AcceptsValidPolicyWithoutCrashing) {
const char* policy =