use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrantSource {
Env,
SecretStore,
}
impl GrantSource {
pub fn as_str(self) -> &'static str {
match self {
GrantSource::Env => "env",
GrantSource::SecretStore => "secret_store",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantSourceSpec {
Env { var: String },
SecretStore { account: String, key: String },
}
impl GrantSourceSpec {
fn kind(&self) -> GrantSource {
match self {
GrantSourceSpec::Env { .. } => GrantSource::Env,
GrantSourceSpec::SecretStore { .. } => GrantSource::SecretStore,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantSpec {
pub name: String,
pub source: GrantSourceSpec,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expose_as_env: Option<String>,
}
impl GrantSpec {
fn resolve(
self,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<SessionGrant, EnvironmentPolicyError> {
let name = self.name.trim();
if name.is_empty() {
return Err(EnvironmentPolicyError::EmptyName);
}
if let Some(var) = self.expose_as_env.as_deref() {
if var.trim().is_empty() {
return Err(EnvironmentPolicyError::EmptyExposeVar {
name: name.to_string(),
});
}
}
let source_kind = self.source.kind();
let source_spec = self.source.clone();
let resolved_ref = match self.source {
GrantSourceSpec::Env { var } => {
let var = var.trim();
if var.is_empty() {
return Err(EnvironmentPolicyError::EmptyEnvVar {
name: name.to_string(),
});
}
let value = env_lookup(var).ok_or_else(|| EnvironmentPolicyError::MissingEnv {
name: name.to_string(),
var: var.to_string(),
})?;
ResolvedRef::EnvSnapshot(value)
}
GrantSourceSpec::SecretStore { account, key } => {
let (account, key) = (account.trim(), key.trim());
if account.is_empty() || key.is_empty() {
return Err(EnvironmentPolicyError::EmptySecretRef {
name: name.to_string(),
});
}
ResolvedRef::SecretStore {
account: account.to_string(),
key: key.to_string(),
}
}
};
Ok(SessionGrant {
name: name.to_string(),
source_kind,
source_spec,
expose_as_env: self.expose_as_env.map(|var| var.trim().to_string()),
resolved_ref,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ResolvedRef {
EnvSnapshot(String),
SecretStore { account: String, key: String },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionGrant {
name: String,
source_kind: GrantSource,
source_spec: GrantSourceSpec,
expose_as_env: Option<String>,
resolved_ref: ResolvedRef,
}
impl SessionGrant {
fn matches_spec(&self, spec: &GrantSpec) -> bool {
self.name == spec.name.trim()
&& self.source_spec == spec.source
&& self.expose_as_env.as_deref() == spec.expose_as_env.as_deref().map(str::trim)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn source_kind(&self) -> GrantSource {
self.source_kind
}
pub fn exposed_env_var(&self) -> Option<&str> {
self.expose_as_env.as_deref()
}
fn exposure(
&self,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Option<Result<(String, String), EnvironmentPolicyError>> {
let var = self.expose_as_env.as_ref()?;
let value = match &self.resolved_ref {
ResolvedRef::EnvSnapshot(value) => value.clone(),
ResolvedRef::SecretStore { account, key } => match resolve_secret(account, key) {
Some(value) => value,
None => {
return Some(Err(EnvironmentPolicyError::MissingSecret {
name: self.name.clone(),
}))
}
},
};
Some(Ok((var.clone(), value)))
}
pub fn receipt(&self) -> GrantReceipt {
GrantReceipt {
name: self.name.clone(),
source_kind: self.source_kind.as_str().to_string(),
exposed_as_env: self.expose_as_env.clone(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EnvironmentPolicyKind {
#[default]
Inherited,
Isolated,
Granted,
}
impl EnvironmentPolicyKind {
pub fn as_str(self) -> &'static str {
match self {
EnvironmentPolicyKind::Inherited => "inherited",
EnvironmentPolicyKind::Isolated => "isolated",
EnvironmentPolicyKind::Granted => "granted",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionEnvironment {
kind: EnvironmentPolicyKind,
launcher_snapshot: BTreeMap<String, String>,
grants: Vec<SessionGrant>,
}
impl SessionEnvironment {
pub fn inherited() -> Self {
Self::launch(EnvironmentPolicyKind::Inherited, Vec::new(), &|name| {
std::env::var(name).ok()
})
.expect("the inherited policy has no fallible grant configuration")
}
pub fn launch(
kind: EnvironmentPolicyKind,
specs: Vec<GrantSpec>,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<Self, EnvironmentPolicyError> {
let mut launcher_snapshot = capture_process_environment();
for name in super::environment_policy::ENV_ALLOWLIST {
if let Some(value) = env_lookup(name) {
launcher_snapshot.insert((*name).to_string(), value);
}
}
Self::launch_from_snapshot(kind, specs, launcher_snapshot, env_lookup)
}
pub fn launch_from_snapshot(
kind: EnvironmentPolicyKind,
specs: Vec<GrantSpec>,
launcher_snapshot: BTreeMap<String, String>,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<Self, EnvironmentPolicyError> {
if !matches!(kind, EnvironmentPolicyKind::Granted) && !specs.is_empty() {
return Err(EnvironmentPolicyError::PolicyForbidsGrants {
policy: kind,
attempted: specs.len(),
});
}
validate_unique_specs(&specs)?;
let grants = specs
.into_iter()
.map(|spec| spec.resolve(env_lookup))
.collect::<Result<Vec<_>, _>>()?;
let launcher_snapshot = if matches!(kind, EnvironmentPolicyKind::Inherited) {
launcher_snapshot
} else {
launcher_snapshot
.into_iter()
.filter(|(name, _)| {
super::environment_policy::ENV_ALLOWLIST.contains(&name.as_str())
})
.collect()
};
Ok(SessionEnvironment {
kind,
launcher_snapshot,
grants,
})
}
pub fn isolated() -> Self {
Self::launch(EnvironmentPolicyKind::Isolated, Vec::new(), &|name| {
std::env::var(name).ok()
})
.expect("the isolated policy has no fallible grant configuration")
}
pub fn kind(&self) -> EnvironmentPolicyKind {
self.kind
}
pub fn is_isolated(&self) -> bool {
matches!(self.kind, EnvironmentPolicyKind::Isolated)
}
pub fn allows_implicit_discovery(&self) -> bool {
matches!(self.kind, EnvironmentPolicyKind::Inherited)
}
pub fn narrow(
&self,
requested: EnvironmentPolicyKind,
specs: Vec<GrantSpec>,
) -> Result<Self, EnvironmentPolicyError> {
match (self.kind, requested) {
(EnvironmentPolicyKind::Inherited, EnvironmentPolicyKind::Inherited)
if specs.is_empty() =>
{
Ok(self.clone())
}
(EnvironmentPolicyKind::Inherited, EnvironmentPolicyKind::Isolated)
if specs.is_empty() =>
{
Ok(Self {
kind: requested,
launcher_snapshot: self.launcher_snapshot.clone(),
grants: Vec::new(),
})
}
(EnvironmentPolicyKind::Inherited, EnvironmentPolicyKind::Granted) => {
if let Some(spec) = specs
.iter()
.find(|spec| matches!(spec.source, GrantSourceSpec::SecretStore { .. }))
{
return Err(EnvironmentPolicyError::ChildPolicyExceedsParent {
parent: self.kind,
requested,
offending_grant: Some(spec.name.trim().to_string()),
detail: "an inherited parent can grant only values in its launch-time environment snapshot; secret-store authority must be granted to the parent first".to_string(),
});
}
let snapshot = self.launcher_snapshot.clone();
Self::launch_from_snapshot(requested, specs, snapshot.clone(), &|name| {
snapshot.get(name).cloned()
})
}
(EnvironmentPolicyKind::Granted, EnvironmentPolicyKind::Isolated)
if specs.is_empty() =>
{
Ok(Self {
kind: requested,
launcher_snapshot: self.launcher_snapshot.clone(),
grants: Vec::new(),
})
}
(EnvironmentPolicyKind::Granted, EnvironmentPolicyKind::Granted) => {
validate_unique_specs(&specs)?;
let mut grants = Vec::with_capacity(specs.len());
for spec in &specs {
let Some(grant) = self.grants.iter().find(|grant| grant.matches_spec(spec))
else {
return Err(EnvironmentPolicyError::ChildPolicyExceedsParent {
parent: self.kind,
requested,
offending_grant: Some(spec.name.trim().to_string()),
detail: format!(
"grant '{}' is not an unchanged subset of the parent grants",
spec.name.trim()
),
});
};
grants.push(grant.clone());
}
Ok(Self {
kind: requested,
launcher_snapshot: self.launcher_snapshot.clone(),
grants,
})
}
_ => Err(EnvironmentPolicyError::ChildPolicyExceedsParent {
parent: self.kind,
requested,
offending_grant: specs.first().map(|spec| spec.name.trim().to_string()),
detail:
"a child may keep or reduce its parent's environment access, never widen it"
.to_string(),
}),
}
}
pub(crate) fn launcher_value(&self, name: &str) -> Option<&str> {
self.launcher_snapshot.get(name).map(String::as_str)
}
pub(crate) fn launcher_snapshot(&self) -> &BTreeMap<String, String> {
&self.launcher_snapshot
}
pub fn grants(&self) -> &[SessionGrant] {
&self.grants
}
pub fn receipts(&self) -> Vec<GrantReceipt> {
self.grants.iter().map(SessionGrant::receipt).collect()
}
pub fn env_exposure(
&self,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Vec<(String, String)>, EnvironmentPolicyError> {
self.grants
.iter()
.filter_map(|grant| grant.exposure(resolve_secret))
.collect()
}
pub fn env_exposure_for(
&self,
var: &str,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Option<String>, EnvironmentPolicyError> {
let Some(grant) = self
.grants
.iter()
.find(|grant| grant.expose_as_env.as_deref() == Some(var))
else {
return Ok(None);
};
grant
.exposure(resolve_secret)
.transpose()
.map(|pair| pair.map(|(_, value)| value))
}
}
fn capture_process_environment() -> BTreeMap<String, String> {
std::env::vars_os()
.filter_map(|(name, value)| Some((name.into_string().ok()?, value.into_string().ok()?)))
.collect()
}
fn validate_unique_specs(specs: &[GrantSpec]) -> Result<(), EnvironmentPolicyError> {
let mut names = BTreeSet::new();
let mut targets = BTreeSet::new();
for spec in specs {
let name = spec.name.trim();
if !name.is_empty() && !names.insert(name) {
return Err(EnvironmentPolicyError::DuplicateGrant {
name: name.to_string(),
});
}
if let Some(target) = spec.expose_as_env.as_deref().map(str::trim) {
if !target.is_empty() && !targets.insert(target) {
return Err(EnvironmentPolicyError::DuplicateExposureTarget {
target: target.to_string(),
});
}
}
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantReceipt {
pub name: String,
pub source_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exposed_as_env: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EnvironmentPolicyError {
EmptyName,
EmptyEnvVar { name: String },
EmptySecretRef { name: String },
EmptyExposeVar { name: String },
MissingEnv { name: String, var: String },
PolicyForbidsGrants {
policy: EnvironmentPolicyKind,
attempted: usize,
},
DuplicateGrant { name: String },
DuplicateExposureTarget { target: String },
ChildPolicyExceedsParent {
parent: EnvironmentPolicyKind,
requested: EnvironmentPolicyKind,
offending_grant: Option<String>,
detail: String,
},
MissingSecret { name: String },
}
impl fmt::Display for EnvironmentPolicyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EnvironmentPolicyError::EmptyName => write!(
f,
"[environment_policy.empty_grant_name] grant spec has an empty name"
),
EnvironmentPolicyError::EmptyEnvVar { name } => {
write!(
f,
"[environment_policy.empty_source_variable] grant '{name}' env source names an empty variable"
)
}
EnvironmentPolicyError::EmptySecretRef { name } => {
write!(
f,
"[environment_policy.empty_secret_reference] grant '{name}' secret source names an empty account/key"
)
}
EnvironmentPolicyError::EmptyExposeVar { name } => {
write!(
f,
"[environment_policy.empty_exposure_target] grant '{name}' expose target is an empty variable"
)
}
EnvironmentPolicyError::MissingEnv { name, var } => write!(
f,
"[environment_policy.source_variable_missing] grant '{name}' env source variable '{var}' is not set in the launcher environment; set it before launch or choose another source"
),
EnvironmentPolicyError::PolicyForbidsGrants { policy, attempted } => write!(
f,
"[environment_policy.grants_forbidden] environment policy '{}' forbids grants, but {attempted} were declared; use 'granted' or remove the grants",
policy.as_str()
),
EnvironmentPolicyError::DuplicateGrant { name } => write!(
f,
"[environment_policy.duplicate_grant] grant name '{name}' is declared more than once; give every grant a unique name"
),
EnvironmentPolicyError::DuplicateExposureTarget { target } => write!(
f,
"[environment_policy.duplicate_exposure_target] environment target '{target}' is exposed by more than one grant; choose one grant for each target"
),
EnvironmentPolicyError::ChildPolicyExceedsParent {
parent,
requested,
offending_grant: _,
detail,
} => write!(
f,
"[environment_policy.child_exceeds_parent] child policy '{}' exceeds parent policy '{}': {detail}",
requested.as_str(),
parent.as_str()
),
EnvironmentPolicyError::MissingSecret { name } => {
write!(
f,
"[environment_policy.secret_unavailable] grant '{name}' is unavailable from the secret store; restore access, rotate the reference, or remove the grant"
)
}
}
}
}
impl std::error::Error for EnvironmentPolicyError {}
impl EnvironmentPolicyError {
pub fn code(&self) -> &'static str {
match self {
Self::EmptyName => "environment_policy.empty_grant_name",
Self::EmptyEnvVar { .. } => "environment_policy.empty_source_variable",
Self::EmptySecretRef { .. } => "environment_policy.empty_secret_reference",
Self::EmptyExposeVar { .. } => "environment_policy.empty_exposure_target",
Self::MissingEnv { .. } => "environment_policy.source_variable_missing",
Self::PolicyForbidsGrants { .. } => "environment_policy.grants_forbidden",
Self::DuplicateGrant { .. } => "environment_policy.duplicate_grant",
Self::DuplicateExposureTarget { .. } => "environment_policy.duplicate_exposure_target",
Self::ChildPolicyExceedsParent { .. } => "environment_policy.child_exceeds_parent",
Self::MissingSecret { .. } => "environment_policy.secret_unavailable",
}
}
pub fn to_json(&self) -> serde_json::Value {
let mut value = serde_json::json!({
"code": self.code(),
"message": self.to_string(),
});
let object = value
.as_object_mut()
.expect("environment policy diagnostic is an object");
match self {
Self::EmptyEnvVar { name }
| Self::EmptySecretRef { name }
| Self::EmptyExposeVar { name }
| Self::MissingSecret { name } => {
object.insert("grant".to_string(), serde_json::json!(name));
}
Self::MissingEnv { name, var } => {
object.insert("grant".to_string(), serde_json::json!(name));
object.insert("sourceVariable".to_string(), serde_json::json!(var));
}
Self::PolicyForbidsGrants { policy, attempted } => {
object.insert("policy".to_string(), serde_json::json!(policy.as_str()));
object.insert("attemptedGrants".to_string(), serde_json::json!(attempted));
}
Self::DuplicateGrant { name } => {
object.insert("grant".to_string(), serde_json::json!(name));
}
Self::DuplicateExposureTarget { target } => {
object.insert("target".to_string(), serde_json::json!(target));
}
Self::ChildPolicyExceedsParent {
parent,
requested,
offending_grant,
detail,
} => {
object.insert(
"parentPolicy".to_string(),
serde_json::json!(parent.as_str()),
);
object.insert(
"requestedPolicy".to_string(),
serde_json::json!(requested.as_str()),
);
object.insert("detail".to_string(), serde_json::json!(detail));
if let Some(grant) = offending_grant {
object.insert("grant".to_string(), serde_json::json!(grant));
}
}
Self::EmptyName => {}
}
value
}
}
#[cfg(test)]
mod tests {
use super::*;
fn no_env(_: &str) -> Option<String> {
None
}
fn env_from(pairs: &'static [(&'static str, &'static str)]) -> impl Fn(&str) -> Option<String> {
move |var: &str| {
pairs
.iter()
.find(|(name, _)| *name == var)
.map(|(_, value)| value.to_string())
}
}
fn env_grant(name: &str, var: &str, expose: Option<&str>) -> GrantSpec {
GrantSpec {
name: name.to_string(),
source: GrantSourceSpec::Env {
var: var.to_string(),
},
expose_as_env: expose.map(str::to_string),
}
}
fn secret_grant(name: &str, account: &str, key: &str, expose: Option<&str>) -> GrantSpec {
GrantSpec {
name: name.to_string(),
source: GrantSourceSpec::SecretStore {
account: account.to_string(),
key: key.to_string(),
},
expose_as_env: expose.map(str::to_string),
}
}
#[test]
fn isolated_rejects_any_grant_at_launch() {
let specs = vec![secret_grant("gh_token", "gh", "token", None)];
let err = SessionEnvironment::launch(EnvironmentPolicyKind::Isolated, specs, &no_env)
.expect_err("isolated must reject grants");
assert_eq!(
err,
EnvironmentPolicyError::PolicyForbidsGrants {
policy: EnvironmentPolicyKind::Isolated,
attempted: 1
}
);
let environment =
SessionEnvironment::launch(EnvironmentPolicyKind::Isolated, vec![], &no_env).unwrap();
assert!(environment.is_isolated());
assert!(environment.grants().is_empty());
assert!(environment.receipts().is_empty());
assert!(SessionEnvironment::isolated().grants().is_empty());
assert_eq!(
EnvironmentPolicyKind::default(),
EnvironmentPolicyKind::Inherited
);
}
#[test]
fn granted_policy_resolves_once_into_typed_record() {
let env = env_from(&[("FIREWORKS_API_KEY", "fw-secret-value")]);
let specs = vec![
env_grant("fireworks", "FIREWORKS_API_KEY", Some("FIREWORKS_API_KEY")),
secret_grant("gh_token", "gh", "token", Some("GH_TOKEN")),
];
let environment =
SessionEnvironment::launch(EnvironmentPolicyKind::Granted, specs, &env).unwrap();
let grants = environment.grants();
assert_eq!(grants.len(), 2);
assert_eq!(grants[0].name(), "fireworks");
assert_eq!(grants[0].source_kind(), GrantSource::Env);
assert_eq!(grants[0].exposed_env_var(), Some("FIREWORKS_API_KEY"));
assert_eq!(grants[1].name(), "gh_token");
assert_eq!(grants[1].source_kind(), GrantSource::SecretStore);
assert_eq!(grants[1].exposed_env_var(), Some("GH_TOKEN"));
let resolve_secret = |account: &str, key: &str| -> Option<String> {
(account == "gh" && key == "token").then(|| "ghp-secret-token".to_string())
};
let mut pairs = environment.env_exposure(&resolve_secret).unwrap();
pairs.sort();
assert_eq!(
pairs,
vec![
(
"FIREWORKS_API_KEY".to_string(),
"fw-secret-value".to_string()
),
("GH_TOKEN".to_string(), "ghp-secret-token".to_string()),
]
);
}
#[test]
fn secret_pointer_is_not_resolved_at_launch() {
let specs = vec![secret_grant("gh_token", "gh", "token", None)];
let environment =
SessionEnvironment::launch(EnvironmentPolicyKind::Granted, specs, &no_env).unwrap();
let never = |_: &str, _: &str| -> Option<String> {
panic!("secret resolver must not run for an unexposed grant")
};
assert!(environment.env_exposure(&never).unwrap().is_empty());
}
#[test]
fn env_grant_snapshots_value_at_launch() {
let at_launch = env_from(&[("TOKEN", "live-at-launch")]);
let specs = vec![env_grant("t", "TOKEN", Some("TOKEN"))];
let environment =
SessionEnvironment::launch(EnvironmentPolicyKind::Granted, specs, &at_launch).unwrap();
let never_secret = |_: &str, _: &str| -> Option<String> { None };
let pairs = environment.env_exposure(&never_secret).unwrap();
assert_eq!(
pairs,
vec![("TOKEN".to_string(), "live-at-launch".to_string())]
);
assert_eq!(
environment.env_exposure(&never_secret).unwrap(),
vec![("TOKEN".to_string(), "live-at-launch".to_string())]
);
}
#[test]
fn restricted_policies_do_not_retain_unrelated_launcher_values() {
let snapshot = BTreeMap::from([
("PATH".to_string(), "/bin".to_string()),
(
"UNRELATED_SECRET".to_string(),
"must-not-be-retained".to_string(),
),
]);
let granted = SessionEnvironment::launch_from_snapshot(
EnvironmentPolicyKind::Granted,
Vec::new(),
snapshot,
&no_env,
)
.unwrap();
assert_eq!(granted.launcher_value("PATH"), Some("/bin"));
assert_eq!(granted.launcher_value("UNRELATED_SECRET"), None);
}
#[test]
fn receipts_record_shape_and_never_the_value() {
let env = env_from(&[("FIREWORKS_API_KEY", "fw-secret-value")]);
let specs = vec![
env_grant("fireworks", "FIREWORKS_API_KEY", Some("FIREWORKS_API_KEY")),
secret_grant("gh_token", "gh", "token", None),
];
let environment =
SessionEnvironment::launch(EnvironmentPolicyKind::Granted, specs, &env).unwrap();
let receipts = environment.receipts();
assert_eq!(
receipts,
vec![
GrantReceipt {
name: "fireworks".to_string(),
source_kind: "env".to_string(),
exposed_as_env: Some("FIREWORKS_API_KEY".to_string()),
},
GrantReceipt {
name: "gh_token".to_string(),
source_kind: "secret_store".to_string(),
exposed_as_env: None,
},
]
);
let json = serde_json::to_string(&receipts).unwrap();
assert!(
!json.contains("fw-secret-value"),
"receipt leaked env value"
);
assert!(!json.contains("gh/token"), "receipt leaked secret pointer");
assert!(json.contains("\"source_kind\":\"env\""));
assert!(json.contains("\"source_kind\":\"secret_store\""));
}
#[test]
fn grant_spec_is_value_free_over_the_wire() {
let spec = env_grant("fireworks", "FIREWORKS_API_KEY", Some("FIREWORKS_API_KEY"));
let json = serde_json::to_string(&spec).unwrap();
let round: GrantSpec = serde_json::from_str(&json).unwrap();
assert_eq!(round, spec);
assert!(json.contains("\"env\""));
assert!(json.contains("FIREWORKS_API_KEY"));
assert_eq!(
serde_json::from_str::<EnvironmentPolicyKind>("\"granted\"").unwrap(),
EnvironmentPolicyKind::Granted
);
assert_eq!(
EnvironmentPolicyKind::default(),
EnvironmentPolicyKind::Inherited
);
}
#[test]
fn missing_env_source_fails_at_launch() {
let specs = vec![env_grant("t", "ABSENT_VAR", None)];
let err = SessionEnvironment::launch(EnvironmentPolicyKind::Granted, specs, &no_env)
.expect_err("absent env var must fail resolution");
assert_eq!(
err,
EnvironmentPolicyError::MissingEnv {
name: "t".to_string(),
var: "ABSENT_VAR".to_string(),
}
);
}
#[test]
fn resolve_rejects_empty_fields() {
let env = env_from(&[("X", "v")]);
assert_eq!(
SessionEnvironment::launch(
EnvironmentPolicyKind::Granted,
vec![env_grant("", "X", None)],
&env
),
Err(EnvironmentPolicyError::EmptyName)
);
assert_eq!(
SessionEnvironment::launch(
EnvironmentPolicyKind::Granted,
vec![env_grant("t", "", None)],
&env
),
Err(EnvironmentPolicyError::EmptyEnvVar {
name: "t".to_string()
})
);
assert_eq!(
SessionEnvironment::launch(
EnvironmentPolicyKind::Granted,
vec![secret_grant("t", "acct", "", None)],
&env
),
Err(EnvironmentPolicyError::EmptySecretRef {
name: "t".to_string()
})
);
assert_eq!(
SessionEnvironment::launch(
EnvironmentPolicyKind::Granted,
vec![env_grant("t", "X", Some(" "))],
&env
),
Err(EnvironmentPolicyError::EmptyExposeVar {
name: "t".to_string()
})
);
}
#[test]
fn duplicate_names_and_targets_fail_with_stable_codes() {
let env = env_from(&[("A", "a"), ("B", "b")]);
let duplicate_name = SessionEnvironment::launch(
EnvironmentPolicyKind::Granted,
vec![
env_grant("token", "A", Some("A")),
env_grant("token", "B", Some("B")),
],
&env,
)
.unwrap_err();
assert_eq!(duplicate_name.code(), "environment_policy.duplicate_grant");
let duplicate_target = SessionEnvironment::launch(
EnvironmentPolicyKind::Granted,
vec![
env_grant("a", "A", Some("TOKEN")),
env_grant("b", "B", Some("TOKEN")),
],
&env,
)
.unwrap_err();
assert_eq!(
duplicate_target.code(),
"environment_policy.duplicate_exposure_target"
);
}
#[test]
fn child_policy_can_only_narrow_parent_authority() {
let snapshot = BTreeMap::from([
("TOKEN".to_string(), "parent-value".to_string()),
("PATH".to_string(), "/bin".to_string()),
]);
let parent = SessionEnvironment::launch_from_snapshot(
EnvironmentPolicyKind::Inherited,
Vec::new(),
snapshot.clone(),
&|name| snapshot.get(name).cloned(),
)
.unwrap();
let child = parent
.narrow(
EnvironmentPolicyKind::Granted,
vec![env_grant("token", "TOKEN", Some("TOKEN"))],
)
.unwrap();
assert_eq!(child.kind(), EnvironmentPolicyKind::Granted);
assert_eq!(child.grants().len(), 1);
let error = child
.narrow(EnvironmentPolicyKind::Inherited, Vec::new())
.unwrap_err();
assert_eq!(error.code(), "environment_policy.child_exceeds_parent");
assert_eq!(error.to_json()["parentPolicy"], "granted");
assert_eq!(error.to_json()["requestedPolicy"], "inherited");
let error = child
.narrow(
EnvironmentPolicyKind::Granted,
vec![env_grant("other", "OTHER_TOKEN", Some("OTHER_TOKEN"))],
)
.unwrap_err();
let diagnostic = error.to_json();
assert_eq!(
diagnostic["code"],
"environment_policy.child_exceeds_parent"
);
assert_eq!(diagnostic["parentPolicy"], "granted");
assert_eq!(diagnostic["requestedPolicy"], "granted");
assert_eq!(diagnostic["grant"], "other");
assert!(diagnostic["message"]
.as_str()
.unwrap()
.contains("unchanged subset of the parent grants"));
}
}