Skip to content

Commit

Permalink
Add support for the local provider in the login function, allow confi…
Browse files Browse the repository at this point in the history
…guring which provider to use
  • Loading branch information
TobiasDeBruijn committed Dec 27, 2024
1 parent fe73998 commit d198ba2
Show file tree
Hide file tree
Showing 13 changed files with 555 additions and 124 deletions.
1 change: 1 addition & 0 deletions server/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions server/database/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ impl User {
Ok(())
}

pub async fn set_is_admin(&self, driver: &Database, is_admin: bool) -> Result<()> {
sqlx::query("UPDATE users SET is_admin = ? WHERE user_id = ?")
.bind(is_admin)
.bind(&self.user_id)
.execute(&**driver)
.await?;

Ok(())
}

pub async fn set_name(&self, driver: &Database, name: &str) -> Result<()> {
sqlx::query("UPDATE users SET name = ? WHERE user_id = ?")
.bind(name)
.bind(&self.user_id)
.execute(&**driver)
.await?;

Ok(())
}

#[instrument(skip(password))]
pub async fn set_password_hash<P: AsRef<str> + Debug>(
&self,
Expand Down
3 changes: 2 additions & 1 deletion server/wilford/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
pem = "3.0.4"
tracing-error = "0.2.0"
rsa = "0.9.6"
bcrypt = "0.16.0"
bcrypt = "0.16.0"
rand = "0.8.5"
149 changes: 149 additions & 0 deletions server/wilford/src/authorization/combined.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use crate::authorization::espo::{EspoAuthorizationProvider, EspoAuthorizationProviderError};
use crate::authorization::local_provider::{
LocalCredentialsProvider, LocalCredentialsProviderError,
};
use crate::authorization::{AuthorizationError, AuthorizationProvider, UserInformation};
use crate::config::{AuthorizationProviderType, Config};
use database::driver::Database;
use espocrm_rs::EspoApiClient;
use std::error::Error;
use std::fmt::Debug;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CombinedAuthorizationProviderError {
#[error(transparent)]
Local(#[from] LocalCredentialsProviderError),
#[error(transparent)]
EspoCrm(#[from] EspoAuthorizationProviderError),
}

/// Convert an authorization error with a generic inner type into a combined authorization error,
/// if E can be turned into a combined error.
fn into_combined_error<E>(
val: AuthorizationError<E>,
) -> AuthorizationError<CombinedAuthorizationProviderError>
where
E: Debug + Error + Into<CombinedAuthorizationProviderError>,
{
match val {
AuthorizationError::Other(e) => AuthorizationError::Other(e.into()),
AuthorizationError::InvalidCredentials => AuthorizationError::InvalidCredentials,
AuthorizationError::AlreadyExists => AuthorizationError::AlreadyExists,
AuthorizationError::TotpNeeded => AuthorizationError::TotpNeeded,
AuthorizationError::UnsupportedOperation => AuthorizationError::UnsupportedOperation,
}
}

/// Abstraction over all the different authorization providers,
/// providing a single object to work with.
pub enum CombinedAuthorizationProvider<'a> {
Local(LocalCredentialsProvider<'a>),
EspoCrm(EspoAuthorizationProvider<'a>),
}

impl<'a> CombinedAuthorizationProvider<'a> {
/// Construct a new authorization provider.
/// The provider backend is chosen dynamically based on the confiuration provided.
///
/// # Panics
///
/// If a backend requiring configuration is configured, but the backend's configuration
/// is not provided.
pub fn new(config: &'a Config, database: &'a Database) -> Self {
match config.authorization_provider {
AuthorizationProviderType::Local => {
Self::Local(LocalCredentialsProvider::new(database))
}
AuthorizationProviderType::EspoCrm => {
if let Some(espo_config) = &config.espo {
let client = EspoApiClient::new(&espo_config.host)
.set_api_key(&espo_config.api_key)
.set_secret_key(&espo_config.secret_key)
.build();

Self::EspoCrm(EspoAuthorizationProvider::new(
client,
&espo_config.host,
&database,
))
} else {
panic!("EspoCrm configured as authorization provider, but no config set for EspoCrm");
}
}
}
}
}

impl<'a> AuthorizationProvider for CombinedAuthorizationProvider<'a> {
type Error = CombinedAuthorizationProviderError;

async fn validate_credentials(
&self,
username: &str,
password: &str,
totp_code: Option<&str>,
) -> Result<UserInformation, AuthorizationError<CombinedAuthorizationProviderError>> {
Ok(match self {
Self::Local(credentials_provider) => credentials_provider
.validate_credentials(username, password, totp_code)
.await
.map_err(into_combined_error)?,

Self::EspoCrm(espocrm) => espocrm
.validate_credentials(username, password, totp_code)
.await
.map_err(into_combined_error)?,
})
}

fn supports_password_change(&self) -> bool {
match self {
Self::Local(credentials_provider) => credentials_provider.supports_password_change(),
Self::EspoCrm(espocrm) => espocrm.supports_password_change(),
}
}

async fn set_password(
&self,
user_id: &str,
new_password: &str,
) -> Result<(), AuthorizationError<Self::Error>> {
Ok(match self {
Self::Local(local) => local
.set_password(user_id, new_password)
.await
.map_err(into_combined_error)?,
Self::EspoCrm(espocrm) => espocrm
.set_password(user_id, new_password)
.await
.map_err(into_combined_error)?,
})
}

fn supports_registration(&self) -> bool {
match self {
Self::Local(credentials_provider) => credentials_provider.supports_registration(),
Self::EspoCrm(espocrm) => espocrm.supports_registration(),
}
}

async fn register_user(
&self,
name: &str,
email: &str,
password: &str,
is_admin: bool,
) -> Result<UserInformation, AuthorizationError<Self::Error>> {
Ok(match self {
Self::Local(credentials_provider) => credentials_provider
.register_user(name, email, password, is_admin)
.await
.map_err(into_combined_error)?,
Self::EspoCrm(espocrm) => espocrm
.register_user(name, email, password, is_admin)
.await
.map_err(into_combined_error)?,
})
}
}
143 changes: 143 additions & 0 deletions server/wilford/src/authorization/espo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use crate::authorization::{AuthorizationError, AuthorizationProvider, UserInformation};
use crate::espo::user::{EspoUser, LoginStatus};
use database::driver::Database;
use database::user::User;
use espocrm_rs::EspoApiClient;
use thiserror::Error;
use tracing::instrument;

#[derive(Debug)]
pub struct EspoAuthorizationProvider<'a> {
database_driver: &'a Database,
espocrm_client: EspoApiClient,
host: &'a str,
}

impl<'a> EspoAuthorizationProvider<'a> {
pub fn new(
espocrm_client: EspoApiClient,
host: &'a str,
database_driver: &'a Database,
) -> Self {
Self {
espocrm_client,
host,
database_driver,
}
}
}

#[derive(Debug, Error)]
pub enum EspoAuthorizationProviderError {
#[error(transparent)]
Espocrm(#[from] reqwest::Error),
#[error(transparent)]
Database(#[from] database::driver::Error),
}

impl<'a> AuthorizationProvider for EspoAuthorizationProvider<'a> {
type Error = EspoAuthorizationProviderError;

#[instrument(skip(self, password))]
async fn validate_credentials(
&self,
username: &str,
password: &str,
totp_code: Option<&str>,
) -> Result<UserInformation, AuthorizationError<Self::Error>> {
// Check the credentials with the EspoCRM instance
// This will yield the user ID
let user_id = match EspoUser::try_login(&self.host, username, password, totp_code)
.await
.map_err(Self::Error::from)?
{
LoginStatus::Ok(user_id) => user_id,
LoginStatus::SecondStepRequired => return Err(AuthorizationError::TotpNeeded),
LoginStatus::Err => return Err(AuthorizationError::InvalidCredentials),
};

// Fetch user information
let espo_user = EspoUser::get_by_id(&self.espocrm_client, &user_id)
.await
.map_err(|e| AuthorizationError::Other(e.into()))?;

if !espo_user.is_active {
return Err(AuthorizationError::InvalidCredentials);
}

// Admin status in EspoCRM
let is_admin = espo_user.user_type.eq("admin");

// While we now know the user is authorized,
// we do want to ensure the user is also represented in our database correctly
let db_user = User::get_by_id(&self.database_driver, &user_id)
.await
.map_err(|e| AuthorizationError::Other(e.into()))?;

if let Some(db_user) = db_user {
// Make sure the database correctly reflects the user's information

// Admin status
if db_user.is_admin != is_admin {
db_user
.set_is_admin(&self.database_driver, is_admin)
.await
.map_err(|e| AuthorizationError::Other(e.into()))?;
}

// Name
if db_user.name.ne(&espo_user.name) {
db_user
.set_name(&self.database_driver, &espo_user.name)
.await
.map_err(|e| AuthorizationError::Other(e.into()))?;
}
} else {
// Create the user in the database

User::new(
self.database_driver,
user_id,
espo_user.name.clone(),
espo_user.email_address.clone(),
is_admin,
)
.await
.map_err(|e| AuthorizationError::Other(e.into()))?;
}

Ok(UserInformation {
id: espo_user.id,
name: espo_user.name,
email: espo_user.email_address,
is_admin: espo_user.user_type.eq("admin"),
})
}

fn supports_password_change(&self) -> bool {
// Password changes are handled in EspoCRM.

false
}

#[instrument(skip_all)]
async fn set_password(&self, _: &str, _: &str) -> Result<(), AuthorizationError<Self::Error>> {
Err(AuthorizationError::UnsupportedOperation)
}

fn supports_registration(&self) -> bool {
// Registration of users is managed in EspoCRM
false
}

#[instrument(skip_all)]
async fn register_user(
&self,
_: &str,
_: &str,
_: &str,
_: bool,
) -> Result<UserInformation, AuthorizationError<Self::Error>> {
Err(AuthorizationError::UnsupportedOperation)
}
}
Loading

0 comments on commit d198ba2

Please sign in to comment.