use std::path::{Path, PathBuf};
use std::sync::Arc;
use cpex_hosts_python::factory::KIND;
use cpex_hosts_python::legacy::IdentityResolvePayload;
use cpex_hosts_python::plugin::{IsolatedPythonPlugin, PythonHookAdapter};
use cpex_hosts_python::testing::{
TempDir, prebuild_venv, python_source, scaffold_plugin, skip, skip_without_python3,
worker_consumes_credentials,
};
use ppe::praxis_policy_core::context::PluginContext;
use ppe::praxis_policy_core::extensions::Extensions;
use ppe::praxis_policy_core::extensions::raw_credentials::{
RawCredentialsExtension, RawInboundToken, TokenKind, TokenRole,
};
use ppe::praxis_policy_core::identity::HOOK_IDENTITY_RESOLVE;
use ppe::praxis_policy_core::plugin::{Plugin, PluginConfig};
use ppe::praxis_policy_core::registry::AnyHookHandler;
const SECRET: &str = "E2E-INBOUND-PLAINTEXT-9f3c1a";
const FIXTURE_CLASS: &str = "cred_pkg.identity_plugin.TokenReadingPlugin";
const FIXTURE_PLUGIN: &str = r#"
import os
from cpex.framework.base import Plugin
from cpex.framework.models import PluginResult
EXPECTED = "E2E-INBOUND-PLAINTEXT-9f3c1a"
class TokenReadingPlugin(Plugin):
"""Reports whether the raw inbound token arrived intact."""
def __init__(self, config):
super().__init__(config)
async def identity_resolve(self, payload, context):
raw = payload.raw_token
# SecretStr: get_secret_value() is the only way to the plaintext.
actual = raw.get_secret_value() if hasattr(raw, "get_secret_value") else raw
if actual == EXPECTED:
verdict = "GOT_TOKEN"
elif not actual:
verdict = "NO_TOKEN"
else:
# Never write the value itself, even on the unexpected path.
verdict = f"OTHER:len={len(actual)}"
marker = os.path.join(os.getcwd(), "credential_verdict.txt")
with open(marker, "w") as f:
f.write(f"{verdict};source={payload.source}")
return PluginResult(continue_processing=True)
"#;
fn require_environment(test_name: &str) -> Option<PathBuf> {
if skip_without_python3(test_name) {
return None;
}
let source = match python_source() {
Ok(source) => source,
Err(reason) => {
skip(test_name, &reason);
return None;
},
};
if !worker_consumes_credentials(&source) {
skip(
test_name,
&format!(
"the cpex source at {} has a worker.py that does not consume the `credential` \
field — the credential path has no consumer there",
source.display()
),
);
return None;
}
Some(source)
}
fn setup(dir: &TempDir, source: &Path, test_name: &str) -> Option<PathBuf> {
let plugin_dir = scaffold_plugin(dir, source, "cred_pkg", "identity_plugin", FIXTURE_PLUGIN);
match prebuild_venv(&plugin_dir, source, FIXTURE_CLASS, "cred_pkg") {
Ok(()) => Some(plugin_dir),
Err(reason) => {
skip(
test_name,
&format!("could not prepare the plugin venv — {reason}"),
);
None
},
}
}
fn extensions_with_token() -> Extensions {
let mut raw = RawCredentialsExtension::default();
raw.inbound_tokens.insert(
TokenRole::User,
RawInboundToken::new(SECRET, "Authorization", TokenKind::Jwt),
);
Extensions {
raw_credentials: Some(Arc::new(raw)),
..Default::default()
}
}
fn plugin_config(capabilities: &[&str]) -> PluginConfig {
PluginConfig {
name: "identity-reader".into(),
kind: KIND.into(),
hooks: vec![HOOK_IDENTITY_RESOLVE.into()],
capabilities: capabilities.iter().map(|c| (*c).to_string()).collect(),
version: Some("1.0.0".into()),
config: Some(serde_json::json!({
"class_name": FIXTURE_CLASS,
"requirements_file": "requirements.txt",
"timeout_secs": 300,
})),
..Default::default()
}
}
fn plugin_for(
config: &PluginConfig,
plugin_dir: &Path,
worker_cwd: &Path,
) -> Arc<IsolatedPythonPlugin> {
Arc::new(
IsolatedPythonPlugin::from_config(config)
.expect("config parses")
.with_plugin_dirs(vec![plugin_dir.display().to_string()])
.with_worker_cwd(worker_cwd.to_path_buf()),
)
}
async fn run_identity_hook(
dir: &TempDir,
plugin_dir: &Path,
capabilities: &[&str],
) -> (String, Vec<String>) {
let config = plugin_config(capabilities);
let plugin = plugin_for(&config, plugin_dir, dir.path());
plugin
.initialize()
.await
.expect("the cached venv is reused and the worker starts");
let payload = IdentityResolvePayload::default();
let adapter = PythonHookAdapter::new(Arc::clone(&plugin), HOOK_IDENTITY_RESOLVE);
let mut ctx = PluginContext::new();
adapter
.invoke(&payload, &extensions_with_token(), &mut ctx)
.await
.expect("the hook round-trips");
let stderr = plugin.worker_stderr().await;
plugin.shutdown().await.expect("shuts down");
let marker = dir.path().join("credential_verdict.txt");
let verdict = std::fs::read_to_string(&marker)
.unwrap_or_else(|e| panic!("the plugin recorded no verdict: {e}"));
(verdict, stderr)
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires python3 + a cpex Python checkout (CPEX_PYTHON_SOURCE); run via `make test-python-e2e`"]
async fn a_capable_plugin_reads_the_raw_token_end_to_end() {
let Some(source) = require_environment("a_capable_plugin_reads_the_raw_token_end_to_end")
else {
return;
};
let dir = TempDir::new();
let Some(plugin_dir) = setup(
&dir,
&source,
"a_capable_plugin_reads_the_raw_token_end_to_end",
) else {
return;
};
let (verdict, _stderr) =
run_identity_hook(&dir, &plugin_dir, &["read_inbound_credentials"]).await;
assert!(
verdict.starts_with("GOT_TOKEN"),
"a plugin declaring read_inbound_credentials must receive the plaintext; got: {verdict}"
);
assert!(
verdict.contains("source=bearer"),
"the token kind should map onto the payload's source field: {verdict}"
);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires python3 + a cpex Python checkout (CPEX_PYTHON_SOURCE); run via `make test-python-e2e`"]
async fn a_non_capable_plugin_on_the_same_hook_receives_no_token() {
let Some(source) =
require_environment("a_non_capable_plugin_on_the_same_hook_receives_no_token")
else {
return;
};
let dir = TempDir::new();
let Some(plugin_dir) = setup(
&dir,
&source,
"a_non_capable_plugin_on_the_same_hook_receives_no_token",
) else {
return;
};
let (verdict, _stderr) = run_identity_hook(&dir, &plugin_dir, &[]).await;
assert!(
verdict.starts_with("NO_TOKEN"),
"a plugin that declared nothing must not receive token material; got: {verdict}"
);
assert!(
!verdict.contains(SECRET),
"the plaintext must not appear in what the plugin observed: {verdict}"
);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires python3 + a cpex Python checkout (CPEX_PYTHON_SOURCE); run via `make test-python-e2e`"]
async fn the_credential_never_reaches_a_log_sink_the_host_controls() {
let Some(source) =
require_environment("the_credential_never_reaches_a_log_sink_the_host_controls")
else {
return;
};
let dir = TempDir::new();
let Some(plugin_dir) = setup(
&dir,
&source,
"the_credential_never_reaches_a_log_sink_the_host_controls",
) else {
return;
};
let (verdict, stderr) =
run_identity_hook(&dir, &plugin_dir, &["read_inbound_credentials"]).await;
assert!(
verdict.starts_with("GOT_TOKEN"),
"precondition: the plugin must have received the token for this test to mean anything; got {verdict}"
);
let captured = stderr.join("\n");
assert!(
!captured.contains(SECRET),
"the plaintext leaked into the worker stderr the host retains:\n{captured}"
);
assert!(
!captured.contains("E2E-INBOUND-PLAINTEXT"),
"a fragment of the plaintext leaked into retained worker stderr:\n{captured}"
);
assert!(
!verdict.contains(SECRET),
"the plugin's recorded output must not contain the plaintext: {verdict}"
);
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires python3 + a cpex Python checkout (CPEX_PYTHON_SOURCE); run via `make test-python-e2e`"]
async fn a_declared_capability_that_cannot_be_honored_fails_closed_end_to_end() {
let Some(source) =
require_environment("a_declared_capability_that_cannot_be_honored_fails_closed_end_to_end")
else {
return;
};
let dir = TempDir::new();
let Some(plugin_dir) = setup(
&dir,
&source,
"a_declared_capability_that_cannot_be_honored_fails_closed_end_to_end",
) else {
return;
};
let config = plugin_config(&["read_inbound_credentials"]);
let plugin = plugin_for(&config, &plugin_dir, dir.path());
plugin.initialize().await.expect("initializes");
let payload = IdentityResolvePayload::default();
let adapter = PythonHookAdapter::new(Arc::clone(&plugin), HOOK_IDENTITY_RESOLVE);
let mut ctx = PluginContext::new();
let result = adapter
.invoke(&payload, &Extensions::default(), &mut ctx)
.await;
let Err(err) = result else {
panic!("a declared capability that cannot be honored must not dispatch successfully");
};
let message = err.to_string();
assert!(
!message.contains(SECRET),
"the fail-closed error must not name any credential: {message}"
);
assert!(
!dir.path().join("credential_verdict.txt").exists(),
"the plugin must not run at all when its declared capability cannot be honored"
);
plugin.shutdown().await.expect("shuts down");
}
#[tokio::test(flavor = "multi_thread")]
#[ignore = "requires python3 + a cpex Python checkout (CPEX_PYTHON_SOURCE); run via `make test-python-e2e`"]
async fn the_real_worker_answers_the_capabilities_handshake() {
let Some(source) = require_environment("the_real_worker_answers_the_capabilities_handshake")
else {
return;
};
let dir = TempDir::new();
let Some(plugin_dir) = setup(
&dir,
&source,
"the_real_worker_answers_the_capabilities_handshake",
) else {
return;
};
let (verdict, _stderr) =
run_identity_hook(&dir, &plugin_dir, &["read_inbound_credentials"]).await;
assert!(
verdict.starts_with("GOT_TOKEN"),
"the worker claimed the `credential` feature, so the token must actually arrive — a claim \
without the behavior is the drift this test exists to catch; got: {verdict}"
);
}