use std::collections::HashMap;
use actix_web::body::EitherBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{http, Error, HttpResponse};
use futures_util::future::LocalBoxFuture;
use regex::Regex;
use crate::http::security::config::Authorizer;
use crate::http::security::user::User;
#[cfg(feature = "http-basic")]
use crate::http::security::http_basic::HttpBasicConfig;
pub struct RequestMatcherAuthorizer {
login_url: &'static str,
matchers: HashMap<String, Access>,
#[cfg(feature = "http-basic")]
http_basic: Option<HttpBasicConfig>,
}
impl RequestMatcherAuthorizer {
pub fn new() -> Self {
RequestMatcherAuthorizer {
login_url: "/login",
matchers: HashMap::new(),
#[cfg(feature = "http-basic")]
http_basic: None,
}
}
pub fn add_matcher(mut self, url_regex: &'static str, access: Access) -> Self {
self.matchers.insert(String::from(url_regex), access);
self
}
pub fn login_url(mut self, url: &'static str) -> Self {
self.login_url = url;
self
}
#[cfg(feature = "http-basic")]
pub fn http_basic(mut self) -> Self {
self.http_basic = Some(HttpBasicConfig::new());
self
}
#[cfg(feature = "http-basic")]
pub fn http_basic_with_config(mut self, config: HttpBasicConfig) -> Self {
self.http_basic = Some(config);
self
}
pub fn matches(&self, path: &str) -> Option<&Access> {
for (pattern, access) in &self.matchers {
if let Ok(re) = Regex::new(pattern) {
if re.is_match(path) {
return Some(access);
}
}
}
None
}
fn check_access(&self, user: &User, access: &Access) -> bool {
user.has_authorities(&access.authorities) || user.has_roles(&access.roles)
}
}
impl Default for RequestMatcherAuthorizer {
fn default() -> Self {
Self::new()
}
}
impl<B: 'static> Authorizer<B> for RequestMatcherAuthorizer {
fn process(
&self,
req: ServiceRequest,
user: Option<&User>,
next: impl FnOnce(ServiceRequest) -> LocalBoxFuture<'static, Result<ServiceResponse<B>, Error>>
+ 'static,
) -> LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>> {
let path = req.path().to_string();
let login_url = self.login_url;
#[cfg(feature = "http-basic")]
let http_basic = self.http_basic.clone();
match user {
Some(u) => {
if path == login_url {
return Box::pin(async move {
Ok(req.into_response(
HttpResponse::Found()
.append_header((http::header::LOCATION, "/"))
.finish()
.map_into_right_body(),
))
});
}
if let Some(access) = self.matches(&path) {
if self.check_access(u, access) {
#[cfg(feature = "audit")]
tracing::debug!(
target: "actix_security::audit",
event_type = "ACCESS_GRANTED",
user = %u.get_username(),
path = %path,
"Access granted"
);
return Box::pin(async move {
let res = next(req).await?;
Ok(res.map_into_left_body())
});
} else {
#[cfg(feature = "audit")]
tracing::warn!(
target: "actix_security::audit",
event_type = "ACCESS_DENIED",
user = %u.get_username(),
path = %path,
required_roles = ?access.roles,
required_authorities = ?access.authorities,
"Access denied: insufficient permissions"
);
return Box::pin(async move {
Ok(req.into_response(
HttpResponse::Forbidden().finish().map_into_right_body(),
))
});
}
}
Box::pin(async move {
let res = next(req).await?;
Ok(res.map_into_left_body())
})
}
None => {
if path == login_url {
Box::pin(async move {
let res = next(req).await?;
Ok(res.map_into_left_body())
})
} else {
#[cfg(feature = "http-basic")]
if let Some(basic_config) = http_basic {
#[cfg(feature = "audit")]
tracing::debug!(
target: "actix_security::audit",
event_type = "AUTHENTICATION_REQUIRED",
path = %path,
auth_method = "http_basic",
"Authentication required (HTTP Basic challenge)"
);
let www_auth = basic_config.www_authenticate_header();
return Box::pin(async move {
Ok(req.into_response(
HttpResponse::Unauthorized()
.append_header((http::header::WWW_AUTHENTICATE, www_auth))
.finish()
.map_into_right_body(),
))
});
}
#[cfg(feature = "audit")]
tracing::debug!(
target: "actix_security::audit",
event_type = "AUTHENTICATION_REQUIRED",
path = %path,
redirect_to = %login_url,
"Authentication required (redirecting to login)"
);
let redirect_url = login_url.to_string();
Box::pin(async move {
Ok(req.into_response(
HttpResponse::Found()
.append_header((http::header::LOCATION, redirect_url))
.finish()
.map_into_right_body(),
))
})
}
}
}
}
}
#[derive(Default)]
pub struct Access {
pub(crate) roles: Vec<String>,
pub(crate) authorities: Vec<String>,
}
impl Access {
pub fn new() -> Self {
Access {
roles: Vec::new(),
authorities: Vec::new(),
}
}
pub fn roles(mut self, roles: Vec<&str>) -> Self {
for role in roles {
let role_str = String::from(role);
if !self.roles.contains(&role_str) {
self.roles.push(role_str);
}
}
self
}
pub fn authorities(mut self, authorities: Vec<&str>) -> Self {
for authority in authorities {
let auth_str = String::from(authority);
if !self.authorities.contains(&auth_str) {
self.authorities.push(auth_str);
}
}
self
}
}