use std::fmt::Display;
use std::sync::Arc;
use super::GateExt;
use crate::accounts::Account;
use crate::authz::access_hierarchy::AccessHierarchy;
use crate::authz::access_policy::AccessPolicy;
use crate::authz::authorization_service::AuthorizationService;
use crate::codecs::Codec;
use crate::codecs::jwt::validation_result::JwtValidationResult;
use crate::codecs::jwt::validation_service::JwtValidationService;
use crate::codecs::jwt::{JwtClaims, RegisteredClaims};
use crate::cookie_template::{CookieTemplate, CookieTemplateBuilderError};
use uuid::Uuid;
#[derive(Clone, Debug)]
pub struct CookieGate<C, R, G>
where
C: Codec,
R: AccessHierarchy + Eq + 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 + Display,
G: Eq,
{
pub fn new_with_codec(issuer: &str, codec: Arc<C>) -> Self
where
R: Default,
{
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 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)
}
pub fn allow_anonymous_with_optional_user(mut self) -> Self {
self.install_optional_extensions = true;
self
}
pub fn require_login(mut self) -> Self
where
R: Default,
{
let baseline = R::default();
self.policy = AccessPolicy::require_role_or_supervisor(baseline);
self
}
pub fn issuer(&self) -> &str {
&self.issuer
}
pub fn policy(&self) -> &AccessPolicy<R, G> {
&self.policy
}
pub fn codec(&self) -> &Arc<C> {
&self.codec
}
pub fn cookie_template(&self) -> &CookieTemplate {
&self.cookie_template
}
pub fn installs_optional_extensions(&self) -> bool {
self.install_optional_extensions
}
}
pub trait CookieGateAdapter<C, R, G>
where
C: Codec,
R: AccessHierarchy + Eq + Display,
G: Eq,
{
type Output;
fn adapt(&self, gate: CookieGate<C, R, G>) -> Self::Output;
}
impl<C, R, Gt> GateExt for super::cookie::CookieGate<C, R, Gt>
where
C: Codec,
R: AccessHierarchy + Eq + Display,
Gt: Eq,
{
}
impl<C, R, Gt, A> crate::gate::adapter::GateAdapter<CookieGate<C, R, Gt>> for A
where
A: CookieGateAdapter<C, R, Gt>,
C: Codec,
R: AccessHierarchy + Eq + Display,
Gt: Eq,
{
type Output = A::Output;
fn adapt(&self, gate: CookieGate<C, R, Gt>) -> Self::Output {
A::adapt(self, gate)
}
}
#[derive(Debug, Clone)]
pub enum CookieEvaluation<R, G>
where
R: AccessHierarchy + Eq + Display + Clone,
G: Eq + Clone,
{
OptionalAnonymous,
OptionalAuthorized {
account: Account<R, G>,
registered_claims: RegisteredClaims,
},
MissingToken,
InvalidToken,
InvalidIssuer {
expected: String,
actual: String,
},
DenyAllPolicy,
PolicyDenied {
account_id: Uuid,
},
Authorized {
account: Account<R, G>,
registered_claims: RegisteredClaims,
},
}
#[derive(Clone, Debug)]
pub struct CookieGateRuntime<C, R, G>
where
C: Codec,
R: AccessHierarchy + Eq + Display + Clone,
G: Eq + Clone,
{
authorization_service: AuthorizationService<R, G>,
jwt_validation_service: JwtValidationService<C>,
install_optional_extensions: bool,
}
impl<C, R, G> CookieGateRuntime<C, R, G>
where
C: Codec<Payload = JwtClaims<Account<R, G>>>,
R: AccessHierarchy + Eq + Display + Clone,
G: Eq + Clone,
{
pub fn new(gate: &CookieGate<C, R, G>) -> Self {
Self {
authorization_service: AuthorizationService::new(gate.policy().clone()),
jwt_validation_service: JwtValidationService::new(
Arc::clone(gate.codec()),
gate.issuer(),
),
install_optional_extensions: gate.installs_optional_extensions(),
}
}
pub fn is_optional(&self) -> bool {
self.install_optional_extensions
}
pub fn evaluate(&self, token: Option<&str>) -> CookieEvaluation<R, G> {
if self.install_optional_extensions {
if let Some(token) = token
&& let JwtValidationResult::Valid(jwt) =
self.jwt_validation_service.validate_token(token)
{
return CookieEvaluation::OptionalAuthorized {
account: jwt.custom_claims,
registered_claims: jwt.registered_claims,
};
}
return CookieEvaluation::OptionalAnonymous;
}
if self.authorization_service.policy_denies_all_access() {
return CookieEvaluation::DenyAllPolicy;
}
let Some(token) = token else {
return CookieEvaluation::MissingToken;
};
match self.jwt_validation_service.validate_token(token) {
JwtValidationResult::Valid(jwt) => {
let account = jwt.custom_claims;
let registered_claims = jwt.registered_claims;
let account_id = account.account_id;
if self.authorization_service.is_authorized(&account) {
CookieEvaluation::Authorized {
account,
registered_claims,
}
} else {
CookieEvaluation::PolicyDenied { account_id }
}
}
JwtValidationResult::InvalidToken => CookieEvaluation::InvalidToken,
JwtValidationResult::InvalidIssuer { expected, actual } => {
CookieEvaluation::InvalidIssuer { expected, actual }
}
}
}
}
impl<C, R, G> CookieGate<C, R, G>
where
C: Codec,
R: AccessHierarchy + Eq + Display + Clone,
G: Eq + Clone,
{
pub fn runtime(&self) -> CookieGateRuntime<C, R, G>
where
C: Codec<Payload = JwtClaims<Account<R, G>>>,
{
CookieGateRuntime::new(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codecs::jwt::{JsonWebToken, JwtClaims};
use crate::groups::Group;
use crate::roles::Role;
use std::sync::Arc;
#[test]
fn optional_mode_missing_token_is_anonymous() {
let gate = CookieGate::<_, Role, Group>::new_with_codec(
"issuer",
Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default()),
)
.allow_anonymous_with_optional_user();
let runtime = gate.runtime();
let result = runtime.evaluate(None);
assert!(matches!(result, CookieEvaluation::OptionalAnonymous));
}
#[test]
fn strict_mode_with_deny_all_policy_short_circuits() {
let gate = CookieGate::<_, Role, Group>::new_with_codec(
"issuer",
Arc::new(JsonWebToken::<JwtClaims<Account<Role, Group>>>::default()),
);
let runtime = gate.runtime();
let result = runtime.evaluate(None);
assert!(matches!(result, CookieEvaluation::DenyAllPolicy));
}
}