use std::collections::BTreeMap;
use std::fmt::Write as _;
use serde::{Deserialize, Serialize};
const MT_MAX_UPLOAD: u64 = 100 * 1024 * 1024;
const MT_MAX_BLOB: u64 = 64 * 1024 * 1024;
const MT_MAX_COMPONENT: u64 = 64 * 1024 * 1024;
const ST_MAX_UPLOAD: u64 = 1024 * 1024 * 1024;
const ST_MAX_BLOB: u64 = 256 * 1024 * 1024;
const ST_MAX_COMPONENT: u64 = 128 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
pub enum SecurityError {
#[error(
"unknown security profile {0:?} (built-ins: multi-tenant, single-tenant, dev; \
or define it under `security.profiles`)"
)]
UnknownProfile(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecurityProfile {
MultiTenant,
SingleTenant,
Dev,
}
impl std::fmt::Display for SecurityProfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for SecurityProfile {
type Err = SecurityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_name(s).ok_or_else(|| SecurityError::UnknownProfile(s.to_string()))
}
}
impl SecurityProfile {
pub fn from_name(name: &str) -> Option<Self> {
match name {
"multi-tenant" => Some(Self::MultiTenant),
"single-tenant" => Some(Self::SingleTenant),
"dev" => Some(Self::Dev),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::MultiTenant => "multi-tenant",
Self::SingleTenant => "single-tenant",
Self::Dev => "dev",
}
}
pub fn preset(self) -> SecurityPosture {
match self {
Self::MultiTenant => SecurityPosture {
allow_unauthenticated_public_bind: false,
max_upload_bytes: MT_MAX_UPLOAD,
allow_site_unix_upstreams: false,
allow_site_private_upstreams: false,
allow_guest_private_egress: false,
allow_guest_self_egress: true,
allow_guest_egress_extra_ca: false,
max_handler_blob_bytes: MT_MAX_BLOB,
max_component_bytes: MT_MAX_COMPONENT,
oidc_require_audience: true,
domain_verify_allow_private: false,
domain_verify_self_serve: true,
allow_shared_kernel_compute: false,
allow_compute_exec: false,
ratelimit_fail_open: false,
allow_implicit_routing: false,
require_pop: false,
require_domain_verification: true,
allow_env_secret_refs: false,
allow_guest_email: false,
allow_guest_mint_capability: false,
max_guest_capability_ttl_secs: 900,
allow_guest_admin_domains: false,
allow_guest_admin_email: false,
allow_guest_admin_site: false,
allow_guest_admin_secrets: false,
require_tenancy_declaration: true,
allow_cross_tenant_db: false,
},
Self::SingleTenant => SecurityPosture {
allow_unauthenticated_public_bind: false,
max_upload_bytes: ST_MAX_UPLOAD,
allow_site_unix_upstreams: true,
allow_site_private_upstreams: true,
allow_guest_private_egress: true,
allow_guest_self_egress: true,
allow_guest_egress_extra_ca: true,
max_handler_blob_bytes: ST_MAX_BLOB,
max_component_bytes: ST_MAX_COMPONENT,
oidc_require_audience: true,
domain_verify_allow_private: true,
domain_verify_self_serve: true,
allow_shared_kernel_compute: true,
allow_compute_exec: false,
ratelimit_fail_open: false,
allow_implicit_routing: true,
require_pop: false,
require_domain_verification: true,
allow_env_secret_refs: true,
allow_guest_email: true,
allow_guest_mint_capability: true,
max_guest_capability_ttl_secs: 3600,
allow_guest_admin_domains: true,
allow_guest_admin_email: true,
allow_guest_admin_site: true,
allow_guest_admin_secrets: true,
require_tenancy_declaration: false,
allow_cross_tenant_db: true,
},
Self::Dev => SecurityPosture {
allow_unauthenticated_public_bind: true,
max_upload_bytes: 0,
allow_site_unix_upstreams: true,
allow_site_private_upstreams: true,
allow_guest_private_egress: true,
allow_guest_self_egress: true,
allow_guest_egress_extra_ca: true,
max_handler_blob_bytes: 0,
max_component_bytes: 0,
oidc_require_audience: false,
domain_verify_allow_private: true,
domain_verify_self_serve: true,
allow_shared_kernel_compute: true,
allow_compute_exec: true,
ratelimit_fail_open: true,
allow_implicit_routing: true,
require_pop: false,
require_domain_verification: false,
allow_env_secret_refs: true,
allow_guest_email: true,
allow_guest_mint_capability: true,
max_guest_capability_ttl_secs: 3600,
allow_guest_admin_domains: true,
allow_guest_admin_email: true,
allow_guest_admin_site: true,
allow_guest_admin_secrets: true,
require_tenancy_declaration: false,
allow_cross_tenant_db: true,
},
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct PostureOverrides {
pub allow_unauthenticated_public_bind: Option<bool>,
pub max_upload_bytes: Option<u64>,
pub allow_site_unix_upstreams: Option<bool>,
pub allow_site_private_upstreams: Option<bool>,
pub allow_guest_private_egress: Option<bool>,
pub allow_guest_self_egress: Option<bool>,
pub allow_guest_egress_extra_ca: Option<bool>,
pub max_handler_blob_bytes: Option<u64>,
pub max_component_bytes: Option<u64>,
pub oidc_require_audience: Option<bool>,
pub domain_verify_allow_private: Option<bool>,
pub domain_verify_self_serve: Option<bool>,
pub allow_shared_kernel_compute: Option<bool>,
pub allow_compute_exec: Option<bool>,
pub ratelimit_fail_open: Option<bool>,
pub allow_implicit_routing: Option<bool>,
pub require_pop: Option<bool>,
pub require_domain_verification: Option<bool>,
pub allow_env_secret_refs: Option<bool>,
pub allow_guest_email: Option<bool>,
pub allow_guest_mint_capability: Option<bool>,
pub max_guest_capability_ttl_secs: Option<u64>,
pub allow_guest_admin_domains: Option<bool>,
pub allow_guest_admin_email: Option<bool>,
pub allow_guest_admin_site: Option<bool>,
pub allow_guest_admin_secrets: Option<bool>,
pub require_tenancy_declaration: Option<bool>,
pub allow_cross_tenant_db: Option<bool>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProjectPostureOverride {
pub require_tenancy_declaration: Option<bool>,
pub allow_cross_tenant_db: Option<bool>,
pub allow_guest_mint_capability: Option<bool>,
pub max_guest_capability_ttl_secs: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResolvedProjectTenancy {
pub require_tenancy_declaration: bool,
pub allow_cross_tenant_db: bool,
pub capability_max_ttl_secs: Option<u64>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct SecurityConfig {
pub profile: Option<String>,
pub profiles: BTreeMap<String, PostureOverrides>,
pub overrides: PostureOverrides,
pub projects: BTreeMap<String, ProjectPostureOverride>,
}
impl SecurityConfig {
fn base_for(&self, name: &str) -> Result<SecurityPosture, SecurityError> {
if let Some(builtin) = SecurityProfile::from_name(name) {
Ok(builtin.preset())
} else if let Some(custom) = self.profiles.get(name) {
Ok(apply(SecurityProfile::MultiTenant.preset(), custom))
} else {
Err(SecurityError::UnknownProfile(name.to_string()))
}
}
pub fn resolve(&self) -> Result<SecurityPosture, SecurityError> {
let name = self.profile.as_deref().unwrap_or("multi-tenant");
Ok(apply(self.base_for(name)?, &self.overrides))
}
pub fn explain(&self) -> Result<String, SecurityError> {
let name = self.profile.as_deref().unwrap_or("multi-tenant");
let p = self.resolve()?;
let o = &self.overrides;
let mut out = String::new();
let _ = writeln!(out, "security profile: {name}");
let mut row = |label: &str, value: String, overridden: bool| {
let src = if overridden { "override" } else { "profile" };
let _ = writeln!(out, " {label:<34} {value:<12} ({src})");
};
row(
"allow_unauthenticated_public_bind",
p.allow_unauthenticated_public_bind.to_string(),
o.allow_unauthenticated_public_bind.is_some(),
);
row(
"max_upload_bytes",
fmt_cap(p.max_upload_bytes),
o.max_upload_bytes.is_some(),
);
row(
"allow_site_unix_upstreams",
p.allow_site_unix_upstreams.to_string(),
o.allow_site_unix_upstreams.is_some(),
);
row(
"allow_site_private_upstreams",
p.allow_site_private_upstreams.to_string(),
o.allow_site_private_upstreams.is_some(),
);
row(
"allow_guest_private_egress",
p.allow_guest_private_egress.to_string(),
o.allow_guest_private_egress.is_some(),
);
row(
"allow_guest_self_egress",
p.allow_guest_self_egress.to_string(),
o.allow_guest_self_egress.is_some(),
);
row(
"allow_guest_egress_extra_ca",
p.allow_guest_egress_extra_ca.to_string(),
o.allow_guest_egress_extra_ca.is_some(),
);
row(
"max_handler_blob_bytes",
fmt_cap(p.max_handler_blob_bytes),
o.max_handler_blob_bytes.is_some(),
);
row(
"max_component_bytes",
fmt_cap(p.max_component_bytes),
o.max_component_bytes.is_some(),
);
row(
"oidc_require_audience",
p.oidc_require_audience.to_string(),
o.oidc_require_audience.is_some(),
);
row(
"domain_verify_allow_private",
p.domain_verify_allow_private.to_string(),
o.domain_verify_allow_private.is_some(),
);
row(
"domain_verify_self_serve",
p.domain_verify_self_serve.to_string(),
o.domain_verify_self_serve.is_some(),
);
row(
"allow_shared_kernel_compute",
p.allow_shared_kernel_compute.to_string(),
o.allow_shared_kernel_compute.is_some(),
);
row(
"allow_compute_exec",
p.allow_compute_exec.to_string(),
o.allow_compute_exec.is_some(),
);
row(
"ratelimit_fail_open",
p.ratelimit_fail_open.to_string(),
o.ratelimit_fail_open.is_some(),
);
row(
"allow_implicit_routing",
p.allow_implicit_routing.to_string(),
o.allow_implicit_routing.is_some(),
);
row(
"require_pop",
p.require_pop.to_string(),
o.require_pop.is_some(),
);
row(
"allow_env_secret_refs",
p.allow_env_secret_refs.to_string(),
o.allow_env_secret_refs.is_some(),
);
row(
"allow_guest_email",
p.allow_guest_email.to_string(),
o.allow_guest_email.is_some(),
);
row(
"allow_guest_mint_capability",
p.allow_guest_mint_capability.to_string(),
o.allow_guest_mint_capability.is_some(),
);
row(
"max_guest_capability_ttl_secs",
p.max_guest_capability_ttl_secs.to_string(),
o.max_guest_capability_ttl_secs.is_some(),
);
row(
"allow_guest_admin_domains",
p.allow_guest_admin_domains.to_string(),
o.allow_guest_admin_domains.is_some(),
);
row(
"allow_guest_admin_email",
p.allow_guest_admin_email.to_string(),
o.allow_guest_admin_email.is_some(),
);
row(
"allow_guest_admin_site",
p.allow_guest_admin_site.to_string(),
o.allow_guest_admin_site.is_some(),
);
row(
"allow_guest_admin_secrets",
p.allow_guest_admin_secrets.to_string(),
o.allow_guest_admin_secrets.is_some(),
);
row(
"require_tenancy_declaration",
p.require_tenancy_declaration.to_string(),
o.require_tenancy_declaration.is_some(),
);
row(
"allow_cross_tenant_db",
p.allow_cross_tenant_db.to_string(),
o.allow_cross_tenant_db.is_some(),
);
Ok(out)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecurityPosture {
pub allow_unauthenticated_public_bind: bool,
pub max_upload_bytes: u64,
pub allow_site_unix_upstreams: bool,
pub allow_site_private_upstreams: bool,
pub allow_guest_private_egress: bool,
pub allow_guest_self_egress: bool,
pub allow_guest_egress_extra_ca: bool,
pub max_handler_blob_bytes: u64,
pub max_component_bytes: u64,
pub oidc_require_audience: bool,
pub domain_verify_allow_private: bool,
pub domain_verify_self_serve: bool,
pub allow_shared_kernel_compute: bool,
pub allow_compute_exec: bool,
pub ratelimit_fail_open: bool,
pub allow_implicit_routing: bool,
pub require_pop: bool,
pub require_domain_verification: bool,
pub allow_env_secret_refs: bool,
pub allow_guest_email: bool,
pub allow_guest_mint_capability: bool,
pub max_guest_capability_ttl_secs: u64,
pub allow_guest_admin_domains: bool,
pub allow_guest_admin_email: bool,
pub allow_guest_admin_site: bool,
pub allow_guest_admin_secrets: bool,
pub require_tenancy_declaration: bool,
pub allow_cross_tenant_db: bool,
}
impl Default for SecurityPosture {
fn default() -> Self {
SecurityProfile::MultiTenant.preset()
}
}
impl SecurityPosture {
pub fn base_project_tenancy(&self) -> ResolvedProjectTenancy {
ResolvedProjectTenancy {
require_tenancy_declaration: self.require_tenancy_declaration,
allow_cross_tenant_db: self.allow_cross_tenant_db,
capability_max_ttl_secs: (self.allow_guest_mint_capability
&& self.max_guest_capability_ttl_secs > 0)
.then_some(self.max_guest_capability_ttl_secs),
}
}
pub fn project_tenancy(&self, ovr: &ProjectPostureOverride) -> ResolvedProjectTenancy {
let mint = ovr
.allow_guest_mint_capability
.unwrap_or(self.allow_guest_mint_capability);
let ttl = ovr
.max_guest_capability_ttl_secs
.unwrap_or(self.max_guest_capability_ttl_secs);
ResolvedProjectTenancy {
require_tenancy_declaration: ovr
.require_tenancy_declaration
.unwrap_or(self.require_tenancy_declaration),
allow_cross_tenant_db: ovr
.allow_cross_tenant_db
.unwrap_or(self.allow_cross_tenant_db),
capability_max_ttl_secs: (mint && ttl > 0).then_some(ttl),
}
}
}
fn apply(mut base: SecurityPosture, o: &PostureOverrides) -> SecurityPosture {
if let Some(v) = o.allow_unauthenticated_public_bind {
base.allow_unauthenticated_public_bind = v;
}
if let Some(v) = o.max_upload_bytes {
base.max_upload_bytes = v;
}
if let Some(v) = o.allow_site_unix_upstreams {
base.allow_site_unix_upstreams = v;
}
if let Some(v) = o.allow_site_private_upstreams {
base.allow_site_private_upstreams = v;
}
if let Some(v) = o.allow_guest_private_egress {
base.allow_guest_private_egress = v;
}
if let Some(v) = o.allow_guest_self_egress {
base.allow_guest_self_egress = v;
}
if let Some(v) = o.allow_guest_egress_extra_ca {
base.allow_guest_egress_extra_ca = v;
}
if let Some(v) = o.max_handler_blob_bytes {
base.max_handler_blob_bytes = v;
}
if let Some(v) = o.max_component_bytes {
base.max_component_bytes = v;
}
if let Some(v) = o.oidc_require_audience {
base.oidc_require_audience = v;
}
if let Some(v) = o.domain_verify_allow_private {
base.domain_verify_allow_private = v;
}
if let Some(v) = o.domain_verify_self_serve {
base.domain_verify_self_serve = v;
}
if let Some(v) = o.allow_shared_kernel_compute {
base.allow_shared_kernel_compute = v;
}
if let Some(v) = o.allow_compute_exec {
base.allow_compute_exec = v;
}
if let Some(v) = o.ratelimit_fail_open {
base.ratelimit_fail_open = v;
}
if let Some(v) = o.allow_implicit_routing {
base.allow_implicit_routing = v;
}
if let Some(v) = o.require_pop {
base.require_pop = v;
}
if let Some(v) = o.require_domain_verification {
base.require_domain_verification = v;
}
if let Some(v) = o.allow_env_secret_refs {
base.allow_env_secret_refs = v;
}
if let Some(v) = o.allow_guest_email {
base.allow_guest_email = v;
}
if let Some(v) = o.allow_guest_mint_capability {
base.allow_guest_mint_capability = v;
}
if let Some(v) = o.max_guest_capability_ttl_secs {
base.max_guest_capability_ttl_secs = v;
}
if let Some(v) = o.allow_guest_admin_domains {
base.allow_guest_admin_domains = v;
}
if let Some(v) = o.allow_guest_admin_email {
base.allow_guest_admin_email = v;
}
if let Some(v) = o.allow_guest_admin_site {
base.allow_guest_admin_site = v;
}
if let Some(v) = o.allow_guest_admin_secrets {
base.allow_guest_admin_secrets = v;
}
if let Some(v) = o.require_tenancy_declaration {
base.require_tenancy_declaration = v;
}
if let Some(v) = o.allow_cross_tenant_db {
base.allow_cross_tenant_db = v;
}
base
}
fn fmt_cap(bytes: u64) -> String {
if bytes == 0 {
"unlimited".to_string()
} else {
bytes.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn per_project_override_tunes_only_the_named_project() {
let base = SecurityProfile::MultiTenant.preset();
let b = base.base_project_tenancy();
assert!(b.require_tenancy_declaration);
assert!(!b.allow_cross_tenant_db);
assert_eq!(b.capability_max_ttl_secs, None);
let ovr = ProjectPostureOverride {
require_tenancy_declaration: None, allow_cross_tenant_db: Some(true),
allow_guest_mint_capability: Some(true),
max_guest_capability_ttl_secs: Some(1800),
};
let r = base.project_tenancy(&ovr);
assert!(r.require_tenancy_declaration); assert!(r.allow_cross_tenant_db); assert_eq!(r.capability_max_ttl_secs, Some(1800));
let ttl_only = ProjectPostureOverride {
max_guest_capability_ttl_secs: Some(3600),
..Default::default()
};
assert_eq!(
base.project_tenancy(&ttl_only).capability_max_ttl_secs,
None
);
let looser = SecurityProfile::SingleTenant.preset();
assert!(looser
.base_project_tenancy()
.capability_max_ttl_secs
.is_some());
let tighten = ProjectPostureOverride {
allow_guest_mint_capability: Some(false),
..Default::default()
};
assert_eq!(
looser.project_tenancy(&tighten).capability_max_ttl_secs,
None
);
}
#[test]
fn default_posture_is_multi_tenant_strict() {
let p = SecurityPosture::default();
assert_eq!(p, SecurityProfile::MultiTenant.preset());
assert!(!p.allow_unauthenticated_public_bind);
assert!(!p.allow_site_unix_upstreams);
assert!(!p.allow_site_private_upstreams);
assert!(p.oidc_require_audience);
assert!(!p.domain_verify_allow_private);
assert!(p.domain_verify_self_serve);
assert!(!p.allow_shared_kernel_compute);
assert!(!p.ratelimit_fail_open);
assert!(!p.allow_implicit_routing);
assert!(!p.require_pop);
assert!(!p.allow_env_secret_refs);
assert_eq!(p.max_upload_bytes, MT_MAX_UPLOAD);
}
#[test]
fn allow_env_secret_refs_follows_the_trust_model() {
assert!(!SecurityProfile::MultiTenant.preset().allow_env_secret_refs);
assert!(SecurityProfile::SingleTenant.preset().allow_env_secret_refs);
assert!(SecurityProfile::Dev.preset().allow_env_secret_refs);
let cfg = SecurityConfig {
overrides: PostureOverrides {
allow_env_secret_refs: Some(true),
..Default::default()
},
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(p.allow_env_secret_refs);
assert!(cfg
.explain()
.unwrap()
.lines()
.any(|l| l.contains("allow_env_secret_refs")
&& l.contains("true")
&& l.contains("override")));
}
#[test]
fn allow_guest_email_follows_the_trust_model() {
assert!(!SecurityProfile::MultiTenant.preset().allow_guest_email);
assert!(SecurityProfile::SingleTenant.preset().allow_guest_email);
assert!(SecurityProfile::Dev.preset().allow_guest_email);
let cfg = SecurityConfig {
overrides: PostureOverrides {
allow_guest_email: Some(true),
..Default::default()
},
..Default::default()
};
assert!(cfg.resolve().unwrap().allow_guest_email);
assert!(cfg
.explain()
.unwrap()
.lines()
.any(|l| l.contains("allow_guest_email")
&& l.contains("true")
&& l.contains("override")));
}
#[test]
fn allow_guest_admin_is_per_surface_and_follows_the_trust_model() {
let mt = SecurityProfile::MultiTenant.preset();
assert!(
!mt.allow_guest_admin_domains
&& !mt.allow_guest_admin_email
&& !mt.allow_guest_admin_site
&& !mt.allow_guest_admin_secrets
);
let st = SecurityProfile::SingleTenant.preset();
assert!(
st.allow_guest_admin_domains
&& st.allow_guest_admin_email
&& st.allow_guest_admin_site
&& st.allow_guest_admin_secrets
);
let cfg = SecurityConfig {
overrides: PostureOverrides {
allow_guest_admin_domains: Some(true),
..Default::default()
},
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(p.allow_guest_admin_domains);
assert!(!p.allow_guest_admin_secrets, "other surfaces stay off");
assert!(cfg
.explain()
.unwrap()
.lines()
.any(|l| l.contains("allow_guest_admin_domains")
&& l.contains("true")
&& l.contains("override")));
}
#[test]
fn tenancy_knobs_follow_the_trust_model() {
let mt = SecurityProfile::MultiTenant.preset();
assert!(mt.require_tenancy_declaration);
assert!(!mt.allow_cross_tenant_db);
for p in [
SecurityProfile::SingleTenant.preset(),
SecurityProfile::Dev.preset(),
] {
assert!(!p.require_tenancy_declaration);
assert!(p.allow_cross_tenant_db);
}
let cfg = SecurityConfig {
overrides: PostureOverrides {
allow_cross_tenant_db: Some(true),
..Default::default()
},
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(p.allow_cross_tenant_db);
assert!(
p.require_tenancy_declaration,
"the declaration gate stays on"
);
assert!(cfg
.explain()
.unwrap()
.lines()
.any(|l| l.contains("allow_cross_tenant_db")
&& l.contains("true")
&& l.contains("override")));
}
#[test]
fn require_pop_defaults_off_everywhere_and_overrides() {
for profile in [
SecurityProfile::MultiTenant,
SecurityProfile::SingleTenant,
SecurityProfile::Dev,
] {
assert!(!profile.preset().require_pop, "{}", profile.as_str());
}
let cfg = SecurityConfig {
overrides: PostureOverrides {
require_pop: Some(true),
..Default::default()
},
..Default::default()
};
assert!(cfg.resolve().unwrap().require_pop);
assert!(cfg
.explain()
.unwrap()
.lines()
.any(|l| l.contains("require_pop") && l.contains("true") && l.contains("override")));
}
#[test]
fn empty_config_resolves_to_multi_tenant() {
let resolved = SecurityConfig::default().resolve().unwrap();
assert_eq!(resolved, SecurityProfile::MultiTenant.preset());
}
#[test]
fn dev_profile_is_loose() {
let cfg = SecurityConfig {
profile: Some("dev".into()),
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(p.allow_unauthenticated_public_bind);
assert!(!p.oidc_require_audience);
assert_eq!(p.max_upload_bytes, 0); assert!(p.ratelimit_fail_open);
assert!(p.allow_implicit_routing);
}
#[test]
fn override_beats_profile() {
let cfg = SecurityConfig {
profile: Some("dev".into()),
overrides: PostureOverrides {
oidc_require_audience: Some(true),
max_upload_bytes: Some(123),
..Default::default()
},
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(
p.oidc_require_audience,
"override must win over the profile"
);
assert_eq!(p.max_upload_bytes, 123);
assert!(p.allow_unauthenticated_public_bind);
}
#[test]
fn custom_profile_layers_over_multi_tenant_baseline() {
let mut profiles = BTreeMap::new();
profiles.insert(
"ci".to_string(),
PostureOverrides {
allow_unauthenticated_public_bind: Some(true),
..Default::default()
},
);
let cfg = SecurityConfig {
profile: Some("ci".into()),
profiles,
..Default::default()
};
let p = cfg.resolve().unwrap();
assert!(p.allow_unauthenticated_public_bind);
assert!(!p.allow_site_private_upstreams);
assert!(p.oidc_require_audience);
}
#[test]
fn unknown_profile_errors() {
let cfg = SecurityConfig {
profile: Some("nope".into()),
..Default::default()
};
assert!(matches!(
cfg.resolve(),
Err(SecurityError::UnknownProfile(name)) if name == "nope"
));
}
#[test]
fn explain_marks_value_source() {
let cfg = SecurityConfig {
profile: Some("multi-tenant".into()),
overrides: PostureOverrides {
max_upload_bytes: Some(0),
..Default::default()
},
..Default::default()
};
let text = cfg.explain().unwrap();
assert!(text.contains("security profile: multi-tenant"));
assert!(text.lines().any(|l| l.contains("max_upload_bytes")
&& l.contains("unlimited")
&& l.contains("override")));
assert!(text
.lines()
.any(|l| l.contains("oidc_require_audience") && l.contains("profile")));
}
}