use std::future::Future;
use std::pin::Pin;
use crate::error::AuthError;
use crate::handle::AllowThem;
use crate::types::{PermissionName, RoleName, SessionToken, User, UserId};
pub type AuthFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, AuthError>> + Send + 'a>>;
pub trait AuthClient: Send + Sync {
fn validate_session<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, Option<User>>;
fn check_role<'a>(&'a self, user_id: &'a UserId, role: &'a RoleName) -> AuthFuture<'a, bool>;
fn check_permission<'a>(
&'a self,
user_id: &'a UserId,
permission: &'a PermissionName,
) -> AuthFuture<'a, bool>;
fn resolve_highest_role<'a>(
&'a self,
user_id: &'a UserId,
hierarchy: &'a [&str],
) -> AuthFuture<'a, Option<String>>;
fn logout<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, ()>;
fn login_url(&self) -> &str;
fn session_cookie_name(&self) -> &str;
}
pub struct EmbeddedAuthClient {
ath: AllowThem,
login_url: String,
}
impl EmbeddedAuthClient {
pub fn new(ath: AllowThem, login_url: impl Into<String>) -> Self {
Self {
ath,
login_url: login_url.into(),
}
}
}
impl AuthClient for EmbeddedAuthClient {
fn validate_session<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, Option<User>> {
Box::pin(async move {
let ttl = self.ath.session_config().ttl;
let session = match self.ath.db().validate_session(token, ttl).await? {
Some(s) => s,
None => return Ok(None),
};
match self.ath.db().get_user(session.user_id).await {
Ok(user) if user.is_active => Ok(Some(user)),
Ok(_) => Ok(None), Err(AuthError::NotFound) => Ok(None), Err(e) => Err(e),
}
})
}
fn check_role<'a>(&'a self, user_id: &'a UserId, role: &'a RoleName) -> AuthFuture<'a, bool> {
Box::pin(async move { self.ath.db().has_role(user_id, role).await })
}
fn check_permission<'a>(
&'a self,
user_id: &'a UserId,
permission: &'a PermissionName,
) -> AuthFuture<'a, bool> {
Box::pin(async move { self.ath.db().has_permission(user_id, permission).await })
}
fn resolve_highest_role<'a>(
&'a self,
user_id: &'a UserId,
hierarchy: &'a [&str],
) -> AuthFuture<'a, Option<String>> {
Box::pin(async move { self.ath.db().resolve_highest_role(user_id, hierarchy).await })
}
fn logout<'a>(&'a self, token: &'a SessionToken) -> AuthFuture<'a, ()> {
Box::pin(async move {
let _ = self.ath.db().delete_session(token).await?;
Ok(())
})
}
fn login_url(&self) -> &str {
&self.login_url
}
fn session_cookie_name(&self) -> &str {
self.ath.session_config().cookie_name
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use chrono::{Duration, Utc};
use super::*;
use crate::handle::AllowThemBuilder;
use crate::sessions::{generate_token, hash_token};
use crate::types::{Email, PermissionName, RoleName};
async fn setup() -> EmbeddedAuthClient {
let ath = AllowThemBuilder::new("sqlite::memory:")
.cookie_secure(false)
.build()
.await
.unwrap();
EmbeddedAuthClient::new(ath, "/login")
}
#[tokio::test]
async fn validate_session_valid_token_returns_user() {
let client = setup().await;
let email = Email::new("valid@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let token = generate_token();
let token_hash = hash_token(&token);
let expires = Utc::now() + Duration::hours(24);
client
.ath
.db()
.create_session(user.id, token_hash, None, None, expires)
.await
.unwrap();
let result = client.validate_session(&token).await.unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().email.as_str(), "valid@example.com");
}
#[tokio::test]
async fn validate_session_expired_token_returns_none() {
let client = setup().await;
let email = Email::new("expired@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let token = generate_token();
let token_hash = hash_token(&token);
let expires = Utc::now() - Duration::hours(1);
client
.ath
.db()
.create_session(user.id, token_hash, None, None, expires)
.await
.unwrap();
let result = client.validate_session(&token).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn validate_session_invalid_token_returns_none() {
let client = setup().await;
let token = generate_token();
let result = client.validate_session(&token).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn validate_session_inactive_user_returns_none() {
let client = setup().await;
let email = Email::new("inactive@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let token = generate_token();
let token_hash = hash_token(&token);
let expires = Utc::now() + Duration::hours(24);
client
.ath
.db()
.create_session(user.id, token_hash, None, None, expires)
.await
.unwrap();
client
.ath
.db()
.update_user_active(user.id, false)
.await
.unwrap();
let result = client.validate_session(&token).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn validate_session_deleted_user_returns_none() {
let client = setup().await;
let email = Email::new("deleted@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let token = generate_token();
let token_hash = hash_token(&token);
let expires = Utc::now() + Duration::hours(24);
client
.ath
.db()
.create_session(user.id, token_hash, None, None, expires)
.await
.unwrap();
client.ath.db().delete_user(user.id).await.unwrap();
let result = client.validate_session(&token).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn check_role_returns_true_when_assigned() {
let client = setup().await;
let email = Email::new("roleuser@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let rn = RoleName::new("admin");
let role = client.ath.db().create_role(&rn, None).await.unwrap();
client
.ath
.db()
.assign_role(&user.id, &role.id)
.await
.unwrap();
let result = client.check_role(&user.id, &rn).await.unwrap();
assert!(result);
}
#[tokio::test]
async fn check_role_returns_false_when_not_assigned() {
let client = setup().await;
let email = Email::new("norole@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let rn = RoleName::new("admin");
let result = client.check_role(&user.id, &rn).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn check_permission_returns_true_direct() {
let client = setup().await;
let email = Email::new("permdirect@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let pn = PermissionName::new("posts:write");
let perm = client.ath.db().create_permission(&pn, None).await.unwrap();
client
.ath
.db()
.assign_permission_to_user(&user.id, &perm.id)
.await
.unwrap();
let result = client.check_permission(&user.id, &pn).await.unwrap();
assert!(result);
}
#[tokio::test]
async fn check_permission_returns_true_via_role() {
let client = setup().await;
let email = Email::new("permviarole@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let rn = RoleName::new("editor");
let role = client.ath.db().create_role(&rn, None).await.unwrap();
let pn = PermissionName::new("posts:read");
let perm = client.ath.db().create_permission(&pn, None).await.unwrap();
client
.ath
.db()
.assign_permission_to_role(&role.id, &perm.id)
.await
.unwrap();
client
.ath
.db()
.assign_role(&user.id, &role.id)
.await
.unwrap();
let result = client.check_permission(&user.id, &pn).await.unwrap();
assert!(result);
}
#[tokio::test]
async fn check_permission_returns_false_when_missing() {
let client = setup().await;
let email = Email::new("noperm@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let pn = PermissionName::new("posts:delete");
let result = client.check_permission(&user.id, &pn).await.unwrap();
assert!(!result);
}
#[tokio::test]
async fn logout_deletes_session() {
let client = setup().await;
let email = Email::new("logout@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let token = generate_token();
let token_hash = hash_token(&token);
let expires = Utc::now() + Duration::hours(24);
client
.ath
.db()
.create_session(user.id, token_hash, None, None, expires)
.await
.unwrap();
client.logout(&token).await.unwrap();
let result = client.validate_session(&token).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn logout_nonexistent_token_succeeds() {
let client = setup().await;
let token = generate_token();
let result = client.logout(&token).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn login_url_returns_configured_path() {
let ath = AllowThemBuilder::new("sqlite::memory:")
.build()
.await
.unwrap();
let client = EmbeddedAuthClient::new(ath, "/login");
assert_eq!(client.login_url(), "/login");
}
#[tokio::test]
async fn session_cookie_name_returns_config_name() {
let ath = AllowThemBuilder::new("sqlite::memory:")
.build()
.await
.unwrap();
let client = EmbeddedAuthClient::new(ath, "/login");
assert_eq!(client.session_cookie_name(), "allowthem_session");
let ath_custom = AllowThemBuilder::new("sqlite::memory:")
.cookie_name("my_session")
.build()
.await
.unwrap();
let client_custom = EmbeddedAuthClient::new(ath_custom, "/login");
assert_eq!(client_custom.session_cookie_name(), "my_session");
}
#[tokio::test]
async fn works_as_arc_dyn_auth_client() {
let ath = AllowThemBuilder::new("sqlite::memory:")
.build()
.await
.unwrap();
let _client: Arc<dyn AuthClient> = Arc::new(EmbeddedAuthClient::new(ath, "/login"));
}
#[tokio::test]
async fn resolve_highest_role_returns_correct_role_via_trait() {
let client = setup().await;
let email = Email::new("roletest@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let roles = client
.ath
.db()
.bootstrap_roles(&["admin", "editor"])
.await
.unwrap();
client
.ath
.db()
.assign_role(&user.id, &roles[1].id) .await
.unwrap();
let result = client
.resolve_highest_role(&user.id, &["admin", "editor"])
.await
.unwrap();
assert_eq!(result, Some("editor".to_owned()));
}
#[tokio::test]
async fn resolve_highest_role_returns_none_via_trait() {
let client = setup().await;
let email = Email::new("noroletest@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let result = client
.resolve_highest_role(&user.id, &["admin", "editor"])
.await
.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn resolve_highest_role_works_as_arc_dyn() {
let client = setup().await;
let email = Email::new("arcdyn@example.com".into()).unwrap();
let user = client
.ath
.db()
.create_user(email, "password123", None, None)
.await
.unwrap();
let roles = client
.ath
.db()
.bootstrap_roles(&["admin", "editor"])
.await
.unwrap();
client
.ath
.db()
.assign_role(&user.id, &roles[0].id) .await
.unwrap();
let arc_client: Arc<dyn AuthClient> = Arc::new(client);
let result = arc_client
.resolve_highest_role(&user.id, &["admin", "editor"])
.await
.unwrap();
assert_eq!(result, Some("admin".to_owned()));
}
}