Skip to content

Instantly share code, notes, and snippets.

@Eraden
Created October 2, 2021 14:02
Show Gist options
  • Save Eraden/23e09f36430ba865f3180d3cc138ab48 to your computer and use it in GitHub Desktop.
Save Eraden/23e09f36430ba865f3180d3cc138ab48 to your computer and use it in GitHub Desktop.
// shared/auth/src/lib.rs
// The MIT License (MIT)
// Copyright (c) 2021 Tsumanu
// Access token with Bearer
use std::future::Future;
use std::rc::Rc;
use actix_web::dev::{Payload, ServiceRequest, ServiceResponse};
use actix_web::web::Data;
use actix_web::{Error, FromRequest, HttpRequest, Result};
use data::{AccessToken, User};
use db_actor::{DbExecutor, Handle};
use futures_util::future::{ready, Ready};
pub use self::middleware::IdentityService;
mod middleware;
#[macro_export]
macro_rules! require_user {
($id: ident) => {{
match $id.user().await {
None => {
log::warn!("Terminating authorized access");
return HttpResponse::Unauthorized().body("");
}
Some(user) => user,
}
}};
}
struct IdentityItem {
access_token: Option<data::AccessToken>,
database: Data<db_actor::DbExecutor>,
}
impl IdentityItem {
pub fn new(
access_token: Option<data::AccessToken>,
database: Data<db_actor::DbExecutor>,
) -> Self {
Self {
database,
access_token,
}
}
}
#[derive(Debug, Clone)]
enum UserState {
NoInitialized,
Anonymouse,
User(Rc<data::User>),
}
#[derive(Clone, Debug)]
pub struct Identity {
database: Data<db_actor::DbExecutor>,
access_token: Option<AccessToken>,
user: UserState,
}
impl Identity {
pub fn new(access_token: Option<AccessToken>, database: Data<DbExecutor>) -> Self {
Self {
database,
access_token,
user: UserState::NoInitialized,
}
}
pub fn access_token(&self) -> Option<&AccessToken> {
self.access_token.as_ref()
}
pub async fn user(&mut self) -> Option<Rc<User>> {
if let Some(u) = self.try_user_ref() {
return u;
}
let token = *self.access_token()?;
self.load_user(token).await;
self.user_ref()
}
fn try_user_ref(&self) -> Option<Option<Rc<User>>> {
match &self.user {
UserState::Anonymouse => Some(None),
UserState::User(user) => Some(Some(user.clone())),
_ => None,
}
}
fn user_ref(&self) -> Option<Rc<User>> {
match &self.user {
UserState::User(user) => Some(user.clone()),
_ => None,
}
}
async fn load_user(&mut self, token: AccessToken) {
self.user = self
.database
.handle(db_actor::AuthorizeUser { token })
.await
.ok()
.map(Rc::new)
.map_or_else(|| UserState::Anonymouse, UserState::User);
log::info!("CURRENT USER IS {:?}", self.user);
}
}
impl FromRequest for Identity {
type Config = ();
type Error = Error;
type Future = Ready<Result<Identity, Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let id = req
.extensions()
.get::<IdentityItem>()
.map(|id| Identity::new(id.access_token, id.database.clone()))
.expect("Identity service unavailable");
ready(Ok(id))
}
}
pub trait IdentityPolicy: Sized + 'static {
type Future: Future<Output = Result<Option<String>, Error>>;
type ResponseFuture: Future<Output = Result<(), Error>>;
fn from_request(&self, req: &mut ServiceRequest) -> Self::Future;
fn to_response<B>(
&self,
identity: Option<String>,
changed: bool,
response: &mut ServiceResponse<B>,
) -> Self::ResponseFuture;
}
pub trait RequestIdentity {
fn get_identity(&self) -> Option<String>;
}
// shared/data/lib.rs
// The MIT License (MIT)
// Copyright (c) 2021 Tsumanu
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct UserId(pub i32);
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ShareToken(pub uuid::Uuid);
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct AccecssToken(pub uuid::Uuid);
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Username(pub String);
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct Email(String);
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct User {
pub id: UserId,
pub email: Email,
pub username: Username,
}
// actors/src/lib.rs
// The MIT License (MIT)
// Copyright (c) 2021 Tsumanu
pub type DbExecutor = sqlx::PgPool;
#[async_trait]
pub trait Handle<Msg> {
type Result;
async fn handle(&self, msg: Msg) -> Result<Self::Result>;
}
pub struct AuthorizeUser {
pub token: data::AccessToken,
}
#[async_trait]
impl Handle<AuthorizeUser> for sqlx::PgPool {
type Result = data::User;
async fn handle(&self, msg: AuthorizeUser) -> Result<Self::Result> {
let user = sqlx::query_as(
r#"
SELECT users.id, users.email, users.username
FROM users
INNER JOIN tokens ON users.id = tokens.user_id
WHERE tokens.access_token = $1
"#,
)
.bind(msg.token)
.fetch_one(self)
.await?;
Ok(user)
}
}
pub struct AllUsers {}
#[async_trait]
impl Handle<AllUsers> for DbExecutor {
type Result = Vec<data::User>;
async fn handle(&self, _msg: AllUsers) -> Result<Self::Result> {
let users = sqlx::query_as(
r#"
SELECT id, email, username
FROM users
"#,
)
.fetch_all(self)
.await?;
Ok(users)
}
}
// shared/auth/src/middleware.rs
// The MIT License (MIT)
// Copyright (c) 2021 Tsumanu
use std::error::Error as StdError;
use std::rc::Rc;
use actix_web::body::{AnyBody, MessageBody};
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::web::Data;
use actix_web::{Error, HttpMessage, Result};
use futures_util::future::{ready, FutureExt as _FE, LocalBoxFuture, Ready, TryFutureExt as TFE_};
use super::*;
static AUTHORIZATION: &str = "Authorization";
static BEARER: &str = "Bearer ";
static TOKEN_LEN: usize = 36;
#[derive(Default, Debug)]
pub struct IdentityService;
impl<S, B> Transform<S, ServiceRequest> for IdentityService
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
B::Error: StdError,
{
type Response = ServiceResponse;
type Error = Error;
type Transform = IdentityServiceMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(IdentityServiceMiddleware {
service: Rc::new(service),
}))
}
}
pub struct IdentityServiceMiddleware<S> {
pub(crate) service: Rc<S>,
}
impl<S> Clone for IdentityServiceMiddleware<S> {
fn clone(&self) -> Self {
Self {
service: Rc::clone(&self.service),
}
}
}
impl<S, B> Service<ServiceRequest> for IdentityServiceMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
B::Error: StdError,
{
type Response = ServiceResponse;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let srv = Rc::clone(&self.service);
async move {
let database = req.app_data::<Data<db_actor::DbExecutor>>();
let token = req
.headers()
.get(AUTHORIZATION)
.and_then(|s| s.to_str().ok())
.filter(|s| {
log::info!("Bearer header {:?}", s);
s.contains(BEARER)
})
.filter(|s| s.len() == BEARER.len() + TOKEN_LEN)
.and_then(|s| {
log::info!("Only token {:?}", &s[BEARER.len()..]);
uuid::Uuid::parse_str(&s[BEARER.len()..]).ok()
})
.map(data::AccessToken::new);
log::info!("token {:?}", token);
req.extensions_mut()
.insert(IdentityItem::new(token, database.unwrap().clone()));
Ok(srv
.call(req)
.await?
.map_body(|_, body| AnyBody::from_message(body)))
}
.map_ok(|res| res.map_body(|_, body| AnyBody::from_message(body)))
.boxed_local()
}
}
// server/src/routes/users.rs
// The MIT License (MIT)
// Copyright (c) 2021 Tsumanu
use actix_web::web::Data;
use actix_web::{get, HttpResponse};
use auth::{require_user, Identity};
use data::*;
use db_actor::{self as db, DbExecutor, Handle};
#[get("/api/v1/users")]
pub async fn users(database: Data<DbExecutor>, mut id: Identity) -> HttpResponse {
let _user = require_user!(id);
let users: Vec<User> = database
.handle(db::AllUsers {})
.await
.ok()
.unwrap_or_default();
let bytes = bincode::serialize(&users).unwrap_or_default();
HttpResponse::Ok().body(bytes)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment