Skip to content

Commit

Permalink
Groundwork for supporting multiple authorization providers
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasDeBruijn committed Nov 3, 2024
1 parent 782623f commit 5642194
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,58 @@
use crate::authorization_backends::{AuthorizationBackend, CheckResult, LoginUser};
use base64::Engine;
use espocrm_rs::{EspoApiClient, Method};
use reqwest::{Result, StatusCode};
use serde::Deserialize;
use tracing::{instrument, trace, warn, warn_span, Instrument};

pub struct EspoAuthorizationClient {
pub client: EspoApiClient,
pub host: String,
}

impl AuthorizationBackend for EspoAuthorizationClient {
type Error = reqwest::Error;

async fn check_credentials(
&self,
username: &str,
password: &str,
two_factor_code: Option<&str>,
) -> std::result::Result<CheckResult, Self::Error> {
let login_status =
EspoUser::try_login(&self.host, username, password, two_factor_code).await?;

match login_status {
LoginStatus::Ok(id) => {
let espo_user = EspoUser::get_by_id(&self.client, &id).await?;
Ok(CheckResult::Ok(LoginUser {
id: espo_user.id,
email: espo_user.email_address,
name: espo_user.name,
is_admin: espo_user.user_type.eq("admin"),
is_active: espo_user.is_active,
}))
}
LoginStatus::SecondStepRequired => Ok(CheckResult::TwoFactorRequired),
LoginStatus::Err => Ok(CheckResult::Invalid),
}
}

async fn get_user(&self, user_id: &str) -> std::result::Result<LoginUser, Self::Error> {
let user = EspoUser::get_by_id(&self.client, &user_id).await?;
Ok(LoginUser {
id: user.id,
email: user.email_address,
is_admin: user.user_type.eq("admin"),
is_active: user.is_active,
name: user.name,
})
}
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EspoUser {
struct EspoUser {
pub id: String,
pub name: String,
pub email_address: String,
Expand All @@ -15,7 +61,7 @@ pub struct EspoUser {
pub user_type: String,
}

pub enum LoginStatus {
enum LoginStatus {
Ok(String),
SecondStepRequired,
Err,
Expand Down
69 changes: 69 additions & 0 deletions server/wilford/src/authorization_backends/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::authorization_backends::espocrm::EspoAuthorizationClient;
use thiserror::Error;

pub mod espocrm;

pub enum AuthorizationProvider {
Espocrm(EspoAuthorizationClient),
}

#[derive(Debug, Error)]
pub enum AuthorizationProviderError {
#[error(transparent)]
Espocrm(#[from] reqwest::Error),
}

#[derive(Debug)]
pub struct LoginUser {
pub id: String,
pub name: String,
pub email: String,
pub is_admin: bool,
pub is_active: bool,
}

#[derive(Debug)]
pub enum CheckResult {
Ok(LoginUser),
TwoFactorRequired,
Invalid,
}

pub trait AuthorizationBackend {
type Error: std::error::Error;

async fn check_credentials(
&self,
username: &str,
password: &str,
two_factor_code: Option<&str>,
) -> Result<CheckResult, Self::Error>;
async fn get_user(&self, user_id: &str) -> Result<LoginUser, Self::Error>;
}

impl AuthorizationBackend for AuthorizationProvider {
type Error = AuthorizationProviderError;

async fn check_credentials(
&self,
username: &str,
password: &str,
two_factor_code: Option<&str>,
) -> Result<CheckResult, Self::Error> {
match self {
Self::Espocrm(client) => {
client
.check_credentials(username, password, two_factor_code)
.await
}
}
.map_err(|e| e.into())
}

async fn get_user(&self, user_id: &str) -> Result<LoginUser, Self::Error> {
match self {
Self::Espocrm(client) => client.get_user(user_id).await,
}
.map_err(|e| e.into())
}
}
6 changes: 6 additions & 0 deletions server/wilford/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ pub struct Config {
pub oidc_signing_key: PathBuf,
pub oidc_public_key: PathBuf,
pub oidc_issuer: String,
pub authorization_provider: AuthorizationProviderType,
}

#[derive(Debug, Deserialize)]
pub enum AuthorizationProviderType {
Espocrm,
}

#[derive(Debug, Deserialize)]
Expand Down
1 change: 0 additions & 1 deletion server/wilford/src/espo/mod.rs

This file was deleted.

34 changes: 25 additions & 9 deletions server/wilford/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use crate::config::{get_config, DefaultClientConfig};
use crate::routes::{OidcPublicKey, OidcSigningKey, WOidcPublicKey, WOidcSigningKey};
use crate::authorization_backends::espocrm::EspoAuthorizationClient;
use crate::authorization_backends::AuthorizationProvider;
use crate::config::{get_config, AuthorizationProviderType, Config, DefaultClientConfig};
use crate::routes::{
OidcPublicKey, OidcSigningKey, WAuthorizationProvider, WOidcPublicKey, WOidcSigningKey,
};
use actix_cors::Cors;
use actix_route_config::Routable;
use actix_web::{web, App, HttpServer};
Expand All @@ -16,8 +20,8 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;

mod authorization_backends;
mod config;
mod espo;
mod response_types;
mod routes;

Expand All @@ -35,15 +39,11 @@ async fn main() -> Result<()> {
&config.database.database,
)
.await?;
let espo_client = EspoApiClient::new(&config.espo.host)
.set_api_key(&config.espo.api_key)
.set_secret_key(&config.espo.secret_key)
.build();

ensure_internal_oauth_client_exists(&database, &config.default_client).await?;

let w_auth_provider = WAuthorizationProvider::new(init_auth_provider(&config));
let w_database = web::Data::new(database);
let w_espo = web::Data::new(espo_client);
let w_oidc_signing_key =
WOidcSigningKey::new(OidcSigningKey(config.read_oidc_signing_key().await?));
let w_oidc_public_key =
Expand All @@ -56,9 +56,9 @@ async fn main() -> Result<()> {
.wrap(TracingLogger::<NoiselessRootSpanBuilder>::new())
.app_data(w_database.clone())
.app_data(w_config.clone())
.app_data(w_espo.clone())
.app_data(w_oidc_signing_key.clone())
.app_data(w_oidc_public_key.clone())
.app_data(w_auth_provider.clone())
.configure(routes::Router::configure)
})
.bind("0.0.0.0:2521")?
Expand All @@ -68,6 +68,22 @@ async fn main() -> Result<()> {
Ok(())
}

fn init_auth_provider(config: &Config) -> AuthorizationProvider {
match config.authorization_provider {
AuthorizationProviderType::Espocrm => {
let espo_client = EspoApiClient::new(&config.espo.host)
.set_api_key(&config.espo.api_key)
.set_secret_key(&config.espo.secret_key)
.build();

AuthorizationProvider::Espocrm(EspoAuthorizationClient {
host: config.espo.host.clone(),
client: espo_client,
})
}
}
}

async fn ensure_internal_oauth_client_exists(
driver: &Database,
config: &DefaultClientConfig,
Expand Down
4 changes: 2 additions & 2 deletions server/wilford/src/routes/appdata.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::authorization_backends::AuthorizationProvider;
use crate::config::Config;
use actix_web::web;
use database::driver::Database;
use espocrm_rs::EspoApiClient;

pub type WDatabase = web::Data<Database>;
pub type WConfig = web::Data<Config>;
pub type WEspo = web::Data<EspoApiClient>;
pub type WAuthorizationProvider = web::Data<AuthorizationProvider>;

pub type WOidcSigningKey = web::Data<OidcSigningKey>;
pub type WOidcPublicKey = web::Data<OidcPublicKey>;
Expand Down
29 changes: 17 additions & 12 deletions server/wilford/src/routes/auth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::espo::user::EspoUser;
use crate::routes::appdata::{WDatabase, WEspo};
use crate::authorization_backends::AuthorizationBackend;
use crate::routes::appdata::WDatabase;
use crate::routes::error::{WebError, WebErrorKind, WebResult};
use crate::routes::WAuthorizationProvider;
use actix_web::cookie::time::OffsetDateTime;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
Expand All @@ -12,7 +13,7 @@ use std::pin::Pin;

#[derive(Debug, Clone)]
pub struct Auth {
pub espo_user_id: String,
pub user_id: String,
pub name: String,
pub is_admin: bool,
token: AccessToken,
Expand All @@ -28,9 +29,9 @@ impl FromRequest for Auth {
.app_data::<WDatabase>()
.expect("Getting AppData for type WDatabase")
.clone();
let espo_client = req
.app_data::<WEspo>()
.expect("Getting AppData for type WEspo")
let auth_provider = req
.app_data::<WAuthorizationProvider>()
.expect("Getting AppData for type WAuthorizationProvider")
.clone();

Box::pin(async move {
Expand All @@ -47,14 +48,16 @@ impl FromRequest for Auth {
None => return Err(WebErrorKind::Unauthorized.into()),
};

let espo_user = EspoUser::get_by_id(&espo_client, &token_info.user_id)
.await
.map_err(|e| WebErrorKind::Espo(e))?;
let stored_user = auth_provider.get_user(&token_info.user_id).await?;

if !stored_user.is_active {
return Err(WebErrorKind::Forbidden.into());
}

Ok(Self {
espo_user_id: espo_user.id,
name: espo_user.name,
is_admin: espo_user.user_type.eq("admin"),
user_id: stored_user.id,
name: stored_user.name,
is_admin: stored_user.is_admin,
token: token_info,
})
})
Expand All @@ -75,7 +78,9 @@ impl Auth {
/// Authentication using a constant token.
/// These tokens are created manually.
pub struct ConstantAccessTokenAuth {
#[allow(unused)]
pub name: String,
#[allow(unused)]
pub token: String,
}

Expand Down
15 changes: 12 additions & 3 deletions server/wilford/src/routes/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ where
}
}

impl WebError {
pub fn new(kind: WebErrorKind) -> Self {
Self {
kind,
context: SpanTrace::capture(),
}
}
}

#[derive(Debug, Error)]
pub enum WebErrorKind {
#[error("Not found")]
Expand All @@ -51,12 +60,12 @@ pub enum WebErrorKind {
Forbidden,
#[error("{0}")]
Database(#[from] database::driver::Error),
#[error("EspoCRM error: {0}")]
Espo(reqwest::Error),
#[error("Internal server error")]
InternalServerError,
#[error("Failed to parse PKCS8 SPKI: {0}")]
RsaPkcs8Spki(#[from] rsa::pkcs8::spki::Error),
#[error("Authorization error: {0}")]
AuthorizationProvider(#[from] crate::authorization_backends::AuthorizationProviderError),
}

impl ResponseError for WebError {
Expand All @@ -68,9 +77,9 @@ impl ResponseError for WebError {
WebErrorKind::Forbidden => StatusCode::FORBIDDEN,
WebErrorKind::InvalidInternalState => StatusCode::INTERNAL_SERVER_ERROR,
WebErrorKind::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
WebErrorKind::Espo(_) => StatusCode::BAD_GATEWAY,
WebErrorKind::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
WebErrorKind::RsaPkcs8Spki(_) => StatusCode::INTERNAL_SERVER_ERROR,
WebErrorKind::AuthorizationProvider(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
Loading

0 comments on commit 5642194

Please sign in to comment.