use crate::auth::{CheckResult, Principal};
use crate::UserId;
use crate::{CheckClient, Namespace, Obj, Permission};
use axum::extract::FromRef;
use axum::http::Method;
use axum::response::{IntoResponse, Redirect, Response};
use axum::{extract::FromRequestParts, http::request::Parts};
use headers::authorization::Bearer;
use headers::{Authorization, Cookie, HeaderMapExt};
use std::error::Error;
use std::fmt::Debug;
use std::future::Future;
use std::marker::PhantomData;
#[derive(Debug, thiserror::Error)]
#[error("Web resource error")]
pub enum WebResourceError {
MissingSession(String),
Forbidden,
MethodNotAllowed,
InternalServerError(Box<dyn Error + 'static>),
Parse(Box<dyn Error + 'static>),
}
impl IntoResponse for WebResourceError {
fn into_response(self) -> Response {
dbg!(&self);
match self {
WebResourceError::MissingSession(loc) => Redirect::to(loc.as_str()).into_response(),
_ => axum::http::StatusCode::NOT_FOUND.into_response(),
}
}
}
pub struct SessionCookieAuth;
pub struct BearerTokenAuth;
pub trait WebResource: Sized {
type Rejection: IntoResponse + Error;
fn namespace(&self) -> Namespace;
fn permission(&self, method: &Method) -> Option<Permission>;
fn parse<S: Send + Sync>(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
fn object(&self) -> Obj;
}
pub struct WithPrincipal<R, A = SessionCookieAuth> {
pub principal: Principal,
pub resource: R,
auth_type: PhantomData<A>,
}
#[allow(dead_code)]
impl<R> WithPrincipal<R> {
pub fn into_principal(self) -> Principal {
self.principal
}
pub fn into_resource(self) -> R {
self.resource
}
pub fn into_principal_and_resource(self) -> (Principal, R) {
(self.principal, self.resource)
}
pub fn map<T>(self, f: impl Fn(R) -> T) -> WithPrincipal<T> {
let resource = f(self.resource);
WithPrincipal {
principal: self.principal,
resource,
auth_type: PhantomData,
}
}
}
#[allow(dead_code)]
impl<R> WithPrincipal<R, BearerTokenAuth> {
pub fn into_principal(self) -> Principal {
self.principal
}
pub fn into_resource(self) -> R {
self.resource
}
pub fn into_principal_and_resource(self) -> (Principal, R) {
(self.principal, self.resource)
}
pub fn map<T>(self, f: impl Fn(R) -> T) -> WithPrincipal<T> {
let resource = f(self.resource);
WithPrincipal {
principal: self.principal,
resource,
auth_type: PhantomData,
}
}
}
impl<S, R> FromRequestParts<S> for WithPrincipal<R, SessionCookieAuth>
where
R: WebResource + Send + 'static,
AuthState: FromRef<S>,
S: Send + Sync,
{
type Rejection = WebResourceError;
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async {
let auth_state = AuthState::from_ref(state);
let resource = R::parse(parts, state)
.await
.map_err(|err| WebResourceError::Parse(Box::new(err)))?;
match parts.headers.typed_get::<Cookie>() {
None => Err(WebResourceError::MissingSession(
auth_state.signin_location.clone(),
)),
Some(c) => match c.get("session") {
None => Err(WebResourceError::MissingSession(
auth_state.signin_location.clone(),
)),
Some(token) => {
let ns = resource.namespace(); let obj = resource.object();
let p = resource
.permission(&parts.method)
.ok_or(WebResourceError::MethodNotAllowed)?;
let u = UserId(token.to_string());
let mut cc = auth_state.check_client;
println!("Permission check: user={:?}, namespace={:?}, object={:?}, permission={:?}",u,ns,obj,p);
match cc.check(ns, obj, p, u, None).await {
Err(err) => {
log::error!("check_client returned error: {:?}", err);
Err(WebResourceError::InternalServerError(Box::new(err)))
}
Ok(CheckResult::Ok(principal)) => {
log::info!("check_client success, principal={:?}", principal);
Ok(WithPrincipal {
principal,
resource,
auth_type: PhantomData,
})
}
Ok(CheckResult::Forbidden(principal)) => {
log::warn!("check_client forbidden, principal={:?}", principal);
Err(WebResourceError::Forbidden)
}
Ok(CheckResult::UnknownPutativeUser) => {
log::warn!("check_client unknown user");
Err(WebResourceError::Forbidden)
}
}
}
},
}
}
}
}
impl<S, R> FromRequestParts<S> for WithPrincipal<R, BearerTokenAuth>
where
R: WebResource + Send + 'static,
AuthState: FromRef<S>,
S: Send + Sync,
{
type Rejection = WebResourceError;
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async {
let auth_state = AuthState::from_ref(state);
let resource = R::parse(parts, state)
.await
.map_err(|err| WebResourceError::Parse(Box::new(err)))?;
match parts.headers.typed_try_get::<Authorization<Bearer>>() {
Err(_) => Err(WebResourceError::MissingSession(
auth_state.signin_location.clone(),
)),
Ok(None) => Err(WebResourceError::MissingSession(
auth_state.signin_location.clone(),
)),
Ok(Some(bearer_auth_value)) => {
let ns = resource.namespace(); let obj = resource.object();
let p = resource
.permission(&parts.method)
.ok_or(WebResourceError::MethodNotAllowed)?;
let u = UserId(bearer_auth_value.token().to_string());
let mut cc = auth_state.check_client;
match cc.check(ns, obj, p, u, None).await {
Err(err) => Err(WebResourceError::InternalServerError(Box::new(err))),
Ok(CheckResult::Ok(principal)) => Ok(WithPrincipal {
principal,
resource,
auth_type: PhantomData,
}),
Ok(CheckResult::Forbidden(_principal)) => Err(WebResourceError::Forbidden),
Ok(CheckResult::UnknownPutativeUser) => Err(WebResourceError::Forbidden),
}
}
}
}
}
}
pub struct WithOptPrincipal<R> {
pub principal: Option<Principal>,
pub resource: R,
}
impl<R> WithOptPrincipal<R> {
}
impl<S, R> FromRequestParts<S> for WithOptPrincipal<R>
where
R: WebResource + Send + 'static,
AuthState: FromRef<S>,
S: Send + Sync,
{
type Rejection = WebResourceError;
fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
async {
let auth_state = AuthState::from_ref(state);
let resource = R::parse(parts, state)
.await
.map_err(|err| WebResourceError::Parse(Box::new(err)))?;
match parts.headers.typed_get::<Cookie>() {
None => Ok(WithOptPrincipal {
principal: None,
resource,
}),
Some(cookies) => match cookies.get("session") {
None => Ok(WithOptPrincipal {
principal: None,
resource,
}),
Some(token) => {
let ns = resource.namespace(); let obj = resource.object();
let p = resource
.permission(&parts.method)
.ok_or(WebResourceError::MethodNotAllowed)?;
let u = UserId(token.to_string());
let mut cc = auth_state.check_client;
match cc.check(ns, obj, p, u, None).await {
Err(err) => Err(WebResourceError::InternalServerError(Box::new(err))),
Ok(CheckResult::Ok(principal)) => Ok(WithOptPrincipal {
principal: Some(principal),
resource,
}),
Ok(CheckResult::Forbidden(_)) => Err(WebResourceError::Forbidden),
Ok(CheckResult::UnknownPutativeUser) => {
Err(WebResourceError::Forbidden)
}
}
}
},
}
}
}
}
#[derive(Clone)]
pub struct AuthState {
pub signin_location: String,
pub check_client: CheckClient,
}
impl AuthState {
pub fn new(check_client: CheckClient, prefix: Option<&str>) -> Self {
AuthState {
check_client,
signin_location: prefix
.map(|p| format!("{}/signin", p))
.unwrap_or_else(|| String::from("/")),
}
}
}