#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SecurityProfile {
pub strict_filter_parsing: bool,
pub scope_propagation_strict: bool,
pub expose_deleted_ids: bool,
pub max_request_body_bytes: usize,
}
const DEFAULT_BODY_LIMIT_BYTES: usize = 2 * 1024 * 1024;
impl SecurityProfile {
#[must_use]
pub const fn secure() -> Self {
Self {
strict_filter_parsing: true,
scope_propagation_strict: true,
expose_deleted_ids: false,
max_request_body_bytes: DEFAULT_BODY_LIMIT_BYTES,
}
}
#[must_use]
pub const fn react_admin() -> Self {
Self {
strict_filter_parsing: false,
scope_propagation_strict: true,
expose_deleted_ids: true,
max_request_body_bytes: DEFAULT_BODY_LIMIT_BYTES,
}
}
#[must_use]
pub const fn legacy() -> Self {
Self {
strict_filter_parsing: false,
scope_propagation_strict: false,
expose_deleted_ids: true,
max_request_body_bytes: DEFAULT_BODY_LIMIT_BYTES,
}
}
}
#[must_use]
pub fn resolve<F>(
extension: Option<axum::Extension<SecurityProfile>>,
fallback: F,
) -> SecurityProfile
where
F: FnOnce() -> SecurityProfile,
{
extension.map_or_else(fallback, |axum::Extension(profile)| profile)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secure_preset_values() {
let p = SecurityProfile::secure();
assert!(p.strict_filter_parsing);
assert!(p.scope_propagation_strict);
assert!(!p.expose_deleted_ids);
assert_eq!(p.max_request_body_bytes, 2 * 1024 * 1024);
}
#[test]
fn test_react_admin_preset_values() {
let p = SecurityProfile::react_admin();
assert!(!p.strict_filter_parsing);
assert!(p.scope_propagation_strict);
assert!(p.expose_deleted_ids);
assert_eq!(p.max_request_body_bytes, 2 * 1024 * 1024);
}
#[test]
fn test_legacy_preset_values() {
let p = SecurityProfile::legacy();
assert!(!p.strict_filter_parsing);
assert!(!p.scope_propagation_strict);
assert!(p.expose_deleted_ids);
assert_eq!(p.max_request_body_bytes, 2 * 1024 * 1024);
}
#[test]
fn test_presets_are_distinct() {
assert_ne!(SecurityProfile::secure(), SecurityProfile::react_admin());
assert_ne!(SecurityProfile::secure(), SecurityProfile::legacy());
assert_ne!(SecurityProfile::react_admin(), SecurityProfile::legacy());
}
#[test]
fn test_struct_update_override() {
let overridden = SecurityProfile {
expose_deleted_ids: true,
..SecurityProfile::secure()
};
assert!(overridden.strict_filter_parsing);
assert!(overridden.scope_propagation_strict);
assert!(overridden.expose_deleted_ids);
assert_eq!(overridden.max_request_body_bytes, DEFAULT_BODY_LIMIT_BYTES);
}
#[test]
fn test_react_admin_keeps_strict_scope() {
assert!(SecurityProfile::react_admin().scope_propagation_strict);
}
#[test]
fn test_body_limit_matches_axum_default() {
assert_eq!(SecurityProfile::secure().max_request_body_bytes, 2_097_152);
assert_eq!(
SecurityProfile::react_admin().max_request_body_bytes,
2_097_152
);
assert_eq!(SecurityProfile::legacy().max_request_body_bytes, 2_097_152);
}
#[test]
fn test_const_constructibility() {
const _SECURE: SecurityProfile = SecurityProfile::secure();
const _RA: SecurityProfile = SecurityProfile::react_admin();
const _LEGACY: SecurityProfile = SecurityProfile::legacy();
}
#[test]
fn test_resolve_extension_wins() {
let resolved = resolve(
Some(axum::Extension(SecurityProfile::secure())),
SecurityProfile::legacy,
);
assert_eq!(resolved, SecurityProfile::secure());
}
#[test]
fn test_resolve_falls_back_when_no_extension() {
let resolved = resolve(None, SecurityProfile::react_admin);
assert_eq!(resolved, SecurityProfile::react_admin());
}
#[test]
fn test_resolve_extension_beats_any_fallback() {
let resolved = resolve(
Some(axum::Extension(SecurityProfile::secure())),
SecurityProfile::legacy,
);
assert!(resolved.strict_filter_parsing);
assert!(!resolved.expose_deleted_ids);
}
}