use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrantSource {
Env,
SecretStore,
Literal,
}
impl GrantSource {
pub fn as_str(self) -> &'static str {
match self {
GrantSource::Env => "env",
GrantSource::SecretStore => "secret_store",
GrantSource::Literal => "literal",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantSourceSpec {
Env { var: String },
SecretStore { account: String, key: String },
Literal { value: String },
}
impl GrantSourceSpec {
fn kind(&self) -> GrantSource {
match self {
GrantSourceSpec::Env { .. } => GrantSource::Env,
GrantSourceSpec::SecretStore { .. } => GrantSource::SecretStore,
GrantSourceSpec::Literal { .. } => GrantSource::Literal,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantAudience {
#[default]
Session,
InProcess,
}
impl GrantAudience {
pub fn is_session(&self) -> bool {
matches!(self, GrantAudience::Session)
}
}
#[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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub for_command: Option<String>,
#[serde(default, skip_serializing_if = "GrantAudience::is_session")]
pub expose_to: GrantAudience,
}
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 for_command = match self.for_command.as_deref().map(str::trim) {
None | Some("") if self.for_command.is_some() => {
return Err(EnvironmentPolicyError::EmptyForCommand {
name: name.to_string(),
});
}
None => None,
Some(command) => {
if self
.expose_as_env
.as_deref()
.map(str::trim)
.is_none_or(|v| v.is_empty())
{
return Err(EnvironmentPolicyError::ForWithoutExpose {
name: name.to_string(),
});
}
if command.contains('/') || command.contains('\\') {
return Err(EnvironmentPolicyError::InvalidForCommand {
name: name.to_string(),
command: command.to_string(),
});
}
Some(command.to_string())
}
};
if self.expose_to == GrantAudience::InProcess {
if self
.expose_as_env
.as_deref()
.map(str::trim)
.is_none_or(|v| v.is_empty())
{
return Err(EnvironmentPolicyError::InProcessWithoutExpose {
name: name.to_string(),
});
}
if for_command.is_some() {
return Err(EnvironmentPolicyError::InProcessWithCommand {
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(),
}
}
GrantSourceSpec::Literal { value } => {
if value
.trim_start()
.starts_with(crate::secrets::SECRET_REF_SCHEME)
{
return Err(EnvironmentPolicyError::LiteralSecretReference {
name: name.to_string(),
});
}
ResolvedRef::Literal(value)
}
};
Ok(SessionGrant {
name: name.to_string(),
source_kind,
source_spec,
expose_as_env: self.expose_as_env.map(|var| var.trim().to_string()),
for_command,
audience: self.expose_to,
resolved_ref,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ResolvedRef {
EnvSnapshot(String),
SecretStore { account: String, key: String },
Literal(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionGrant {
name: String,
source_kind: GrantSource,
source_spec: GrantSourceSpec,
expose_as_env: Option<String>,
for_command: Option<String>,
audience: GrantAudience,
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)
&& self.for_command.as_deref() == spec.for_command.as_deref().map(str::trim)
&& self.audience == spec.expose_to
}
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()
}
pub fn for_command(&self) -> Option<&str> {
self.for_command.as_deref()
}
pub fn audience(&self) -> GrantAudience {
self.audience
}
fn reaches_in_process(&self) -> bool {
self.for_command.is_none()
}
pub fn reaches_spawn(&self, program: Option<&str>) -> bool {
if self.audience == GrantAudience::InProcess {
return false;
}
match (self.for_command.as_deref(), program) {
(None, _) => true,
(Some(expected), Some(program)) => command_basename(program) == expected,
(Some(_), None) => false,
}
}
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) | ResolvedRef::Literal(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(),
for_command: self.for_command.clone(),
exposed_to: self.audience,
}
}
}
pub fn command_basename(program: &str) -> &str {
let name = program
.rsplit(['/', '\\'])
.next()
.filter(|name| !name.is_empty())
.unwrap_or(program);
strip_windows_executable_suffix(name)
}
fn strip_windows_executable_suffix(name: &str) -> &str {
const SUFFIXES: &[&str] = &[".exe", ".bat", ".cmd", ".com"];
for suffix in SUFFIXES {
let Some(stem_len) = name.len().checked_sub(suffix.len()).filter(|len| *len > 0) else {
continue;
};
if let Some((stem, tail)) = name.split_at_checked(stem_len) {
if tail.eq_ignore_ascii_case(suffix) {
return stem;
}
}
}
name
}
#[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) {
insert_env_value(&mut launcher_snapshot, name, 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::allowlist_admits(name))
.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> {
if let Some(value) = self.launcher_snapshot.get(name) {
return Some(value.as_str());
}
if cfg!(windows) {
return self
.launcher_snapshot
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str());
}
None
}
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 admitted_environment_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.launcher_snapshot.keys().cloned().collect();
for grant in &self.grants {
if grant.audience == GrantAudience::InProcess {
continue;
}
if let Some(var) = grant.expose_as_env.clone() {
names.push(var);
}
}
names.sort();
names.dedup();
names
}
pub fn env_exposure(
&self,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Vec<(String, String)>, EnvironmentPolicyError> {
self.child_exposure(None, resolve_secret)
}
pub fn env_exposure_for_command(
&self,
program: &str,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Vec<(String, String)>, EnvironmentPolicyError> {
self.child_exposure(Some(program), resolve_secret)
}
fn child_exposure(
&self,
program: Option<&str>,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Vec<(String, String)>, EnvironmentPolicyError> {
self.grants
.iter()
.filter(|grant| grant.reaches_spawn(program))
.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.reaches_in_process() && 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 insert_env_value_case_insensitive(
map: &mut BTreeMap<String, String>,
name: &str,
value: String,
) {
let existing_key = map
.keys()
.find(|key| key.eq_ignore_ascii_case(name))
.cloned();
map.insert(existing_key.unwrap_or_else(|| name.to_string()), value);
}
fn insert_env_value(map: &mut BTreeMap<String, String>, name: &str, value: String) {
if cfg!(windows) {
insert_env_value_case_insensitive(map, name, value);
} else {
map.insert(name.to_string(), value);
}
}
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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub for_command: Option<String>,
#[serde(default, skip_serializing_if = "GrantAudience::is_session")]
pub exposed_to: GrantAudience,
}
impl GrantReceipt {
pub fn child_visible_env(&self) -> Option<&str> {
match self.exposed_to {
GrantAudience::Session => self.exposed_as_env.as_deref(),
GrantAudience::InProcess => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EnvironmentPolicyError {
EmptyName,
EmptyEnvVar { name: String },
LiteralSecretReference { name: String },
EmptySecretRef { name: String },
EmptyExposeVar { name: String },
EmptyForCommand { name: String },
ForWithoutExpose { name: String },
InvalidForCommand { name: String, command: String },
InProcessWithoutExpose { name: String },
InProcessWithCommand { 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::LiteralSecretReference { name } => {
write!(
f,
"[environment_policy.literal_secret_reference] grant '{name}' literal source states a secret reference; declare it as a secret_store source instead"
)
}
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::EmptyForCommand { name } => {
write!(
f,
"[environment_policy.empty_for_command] grant '{name}' for_command binding is empty; name the command basename (for example 'gh')"
)
}
EnvironmentPolicyError::ForWithoutExpose { name } => {
write!(
f,
"[environment_policy.for_without_expose] grant '{name}' declares for_command without expose_as_env; command binding only applies to an exposed environment variable"
)
}
EnvironmentPolicyError::InvalidForCommand { name, command } => {
write!(
f,
"[environment_policy.invalid_for_command] grant '{name}' for_command '{command}' must be a command basename, not a path"
)
}
EnvironmentPolicyError::InProcessWithoutExpose { name } => write!(
f,
"[environment_policy.in_process_without_expose] grant '{name}' declares expose_to in_process without expose_as_env; name the variable Harn's own readers should find it under"
),
EnvironmentPolicyError::InProcessWithCommand { name } => write!(
f,
"[environment_policy.in_process_with_command] grant '{name}' declares expose_to in_process with for_command; an in-process grant reaches no spawned command, so remove one of the two"
),
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::LiteralSecretReference { .. } => "environment_policy.literal_secret_reference",
Self::EmptySecretRef { .. } => "environment_policy.empty_secret_reference",
Self::EmptyExposeVar { .. } => "environment_policy.empty_exposure_target",
Self::EmptyForCommand { .. } => "environment_policy.empty_for_command",
Self::ForWithoutExpose { .. } => "environment_policy.for_without_expose",
Self::InvalidForCommand { .. } => "environment_policy.invalid_for_command",
Self::InProcessWithoutExpose { .. } => "environment_policy.in_process_without_expose",
Self::InProcessWithCommand { .. } => "environment_policy.in_process_with_command",
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::LiteralSecretReference { name }
| Self::EmptyEnvVar { name }
| Self::EmptySecretRef { name }
| Self::EmptyExposeVar { name }
| Self::EmptyForCommand { name }
| Self::ForWithoutExpose { name }
| Self::InProcessWithoutExpose { name }
| Self::InProcessWithCommand { name }
| Self::MissingSecret { name } => {
object.insert("grant".to_string(), serde_json::json!(name));
}
Self::InvalidForCommand { name, command } => {
object.insert("grant".to_string(), serde_json::json!(name));
object.insert("forCommand".to_string(), serde_json::json!(command));
}
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)]
#[path = "session_environment_tests.rs"]
mod tests;