pub(crate) mod cookie_service;
use self::cookie_service::CookieGateService;
use crate::authz::{AccessHierarchy, AccessPolicy};
use crate::codecs::Codec;
use crate::cookie_template::{CookieTemplate, CookieTemplateBuilderError};
use std::sync::Arc;
use tower::Layer;
#[derive(Clone)]
pub struct CookieGate<C, R, G>
where
C: Codec,
R: AccessHierarchy + Eq + std::fmt::Display,
G: Eq,
{
issuer: String,
policy: AccessPolicy<R, G>,
codec: Arc<C>,
cookie_template: CookieTemplate,
install_optional_extensions: bool,
}
impl<C, R, G> CookieGate<C, R, G>
where
C: Codec,
R: AccessHierarchy + Eq + std::fmt::Display,
G: Eq,
{
pub(super) fn new_with_codec(issuer: &str, codec: Arc<C>) -> Self {
Self {
issuer: issuer.to_string(),
policy: AccessPolicy::deny_all(),
codec,
cookie_template: CookieTemplate::recommended(),
install_optional_extensions: false,
}
}
pub fn with_policy(mut self, policy: AccessPolicy<R, G>) -> Self {
self.policy = policy;
self
}
pub fn with_cookie_template(mut self, template: CookieTemplate) -> Self {
self.cookie_template = template;
self
}
pub fn allow_anonymous_with_optional_user(mut self) -> Self {
self.install_optional_extensions = true;
self
}
pub fn configure_cookie_template<F>(mut self, f: F) -> Result<Self, CookieTemplateBuilderError>
where
F: FnOnce(CookieTemplate) -> CookieTemplate,
{
let template = f(CookieTemplate::recommended());
template.validate()?;
self.cookie_template = template;
Ok(self)
}
#[cfg(feature = "prometheus")]
pub fn with_prometheus_metrics(self) -> Self {
let _ = crate::audit::prometheus_metrics::install_prometheus_metrics();
self
}
#[cfg(feature = "prometheus")]
pub fn with_prometheus_registry(self, registry: &prometheus::Registry) -> Self {
let _ =
crate::audit::prometheus_metrics::install_prometheus_metrics_with_registry(registry);
self
}
}
impl<S, C, R, G> Layer<S> for CookieGate<C, R, G>
where
C: Codec,
R: AccessHierarchy + Eq + std::fmt::Display,
G: Eq + Clone,
{
type Service = CookieGateService<C, R, G, S>;
fn layer(&self, inner: S) -> Self::Service {
if self.install_optional_extensions {
CookieGateService::new_with_optional_extensions(
inner,
&self.issuer,
Arc::clone(&self.codec),
self.cookie_template.clone(),
)
} else {
CookieGateService::new(
inner,
&self.issuer,
self.policy.clone(),
Arc::clone(&self.codec),
self.cookie_template.clone(),
)
}
}
}
impl<C, R, G> CookieGate<C, R, G>
where
C: Codec,
R: AccessHierarchy + std::fmt::Display,
G: Eq,
{
pub fn require_login(mut self) -> Self {
let baseline = R::default();
self.policy = AccessPolicy::require_role_or_supervisor(baseline);
self
}
}
#[cfg(test)]
mod tests {
use super::{super::*, *};
use crate::accounts::Account;
use crate::groups::Group;
use crate::roles::Role;
use crate::codecs::jwt::{JsonWebToken, JwtClaims};
use std::sync::Arc;
#[test]
fn cookie_creates_gate_with_deny_all_policy() {
let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
let gate: CookieGate<_, Role, Group> = Gate::cookie("test-app", jwt_codec);
assert_eq!(gate.issuer, "test-app");
assert!(gate.policy.denies_all());
}
#[test]
fn require_login_creates_gate_with_user_or_supervisor_policy() {
let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
let gate: CookieGate<_, Role, Group> = Gate::cookie("test-app", jwt_codec).require_login();
assert_eq!(gate.issuer, "test-app");
assert!(!gate.policy.denies_all());
assert!(gate.policy.has_requirements());
let role_requirements = gate.policy.role_requirements();
assert_eq!(role_requirements.len(), 1);
assert_eq!(role_requirements[0].role, Role::User);
assert!(role_requirements[0].allow_supervisor_access);
assert!(gate.policy.group_requirements().is_empty());
assert!(gate.policy.permission_requirements().is_empty());
}
#[test]
fn with_policy_updates_access_policy() {
let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
let custom_policy: AccessPolicy<Role, Group> = AccessPolicy::require_role(Role::Admin);
let gate: CookieGate<_, Role, Group> =
Gate::cookie("test-app", jwt_codec).with_policy(custom_policy);
assert!(!gate.policy.denies_all());
let role_requirements = gate.policy.role_requirements();
assert_eq!(role_requirements.len(), 1);
assert_eq!(role_requirements[0].role, Role::Admin);
assert!(!role_requirements[0].allow_supervisor_access);
}
#[test]
fn with_cookie_template_updates_cookie_configuration() {
let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
let custom_template = CookieTemplate::recommended().name("custom-cookie");
let _gate: CookieGate<_, Role, Group> =
Gate::cookie("test-app", jwt_codec).with_cookie_template(custom_template);
}
#[test]
#[allow(clippy::unwrap_used)]
fn configure_cookie_template_uses_closure() {
let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
let _gate: CookieGate<_, Role, Group> = Gate::cookie("test-app", jwt_codec)
.configure_cookie_template(|tpl| {
tpl.name("configured-cookie")
.persistent(::cookie::time::Duration::hours(2))
})
.unwrap();
}
#[test]
fn require_login_allows_all_role_hierarchy() {
let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
let gate: CookieGate<_, Role, Group> = Gate::cookie("test-app", jwt_codec).require_login();
let (role_requirements, _, _) = gate.policy.into_components();
assert_eq!(role_requirements.len(), 1);
let requirement = &role_requirements[0];
assert_eq!(requirement.role, Role::User);
assert!(requirement.allow_supervisor_access);
}
#[test]
#[allow(clippy::unwrap_used)]
fn require_login_can_be_chained_with_other_methods() {
let jwt_codec = Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default());
let gate: CookieGate<_, Role, Group> = Gate::cookie("test-app", jwt_codec)
.require_login()
.configure_cookie_template(|tpl| tpl.name("custom-auth"))
.unwrap();
assert!(!gate.policy.denies_all());
assert!(gate.policy.has_requirements());
}
}