-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for the local provider in the login function, allow confi…
…guring which provider to use
- Loading branch information
1 parent
fe73998
commit d198ba2
Showing
13 changed files
with
555 additions
and
124 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)?, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.