feat(users): setup user authentication methods schema and apis (#4999)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Mani Chandra Dulam <mani.dchandra@juspay.in>
This commit is contained in:
Apoorv Dixit
2024-06-21 16:29:17 +05:30
committed by GitHub
parent 5cde7ee034
commit 2005d3df9f
29 changed files with 888 additions and 16 deletions

View File

@ -644,4 +644,7 @@ enabled = false
global_tenant = { schema = "public", redis_key_prefix = "" }
[multitenancy.tenants]
public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} # schema -> Postgres db schema, redis_key_prefix -> redis key distinguisher, base_url -> url of the tenant
public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} # schema -> Postgres db schema, redis_key_prefix -> redis key distinguisher, base_url -> url of the tenant
[user_auth_methods]
encryption_key = "" # Encryption key used for encrypting data in user_authentication_methods table

View File

@ -259,4 +259,7 @@ enabled = false
global_tenant = { schema = "public", redis_key_prefix = "" }
[multitenancy.tenants]
public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[user_auth_methods]
encryption_key = "user_auth_table_encryption_key" # Encryption key used for encrypting data in user_authentication_methods table

View File

@ -654,3 +654,6 @@ global_tenant = { schema = "public", redis_key_prefix = "" }
[multitenancy.tenants]
public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[user_auth_methods]
encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F"

View File

@ -507,4 +507,7 @@ enabled = false
global_tenant = { schema = "public", redis_key_prefix = "" }
[multitenancy.tenants]
public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
public = { name = "hyperswitch", base_url = "http://localhost:8080", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}
[user_auth_methods]
encryption_key = "A8EF32E029BC3342E54BF2E172A4D7AA43E8EF9D2C3A624A9F04E2EF79DC698F"

View File

@ -11,14 +11,15 @@ use crate::user::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
},
AcceptInviteFromEmailRequest, AuthorizeResponse, BeginTotpResponse, ChangePasswordRequest,
ConnectAccountRequest, CreateInternalUserRequest, DashboardEntryResponse,
ForgotPasswordRequest, GetUserDetailsResponse, GetUserRoleDetailsRequest,
GetUserRoleDetailsResponse, InviteUserRequest, ListUsersResponse, ReInviteUserRequest,
RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest,
SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest,
TokenOrPayloadResponse, TokenResponse, TwoFactorAuthStatusResponse,
UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest,
VerifyRecoveryCodeRequest, VerifyTotpRequest,
ConnectAccountRequest, CreateInternalUserRequest, CreateUserAuthenticationMethodRequest,
DashboardEntryResponse, ForgotPasswordRequest, GetUserAuthenticationMethodsRequest,
GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
InviteUserRequest, ListUsersResponse, ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest,
RotatePasswordRequest, SendVerifyEmailRequest, SignInResponse, SignUpRequest,
SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenOrPayloadResponse, TokenResponse,
TwoFactorAuthStatusResponse, UpdateUserAccountDetailsRequest,
UpdateUserAuthenticationMethodRequest, UserFromEmailRequest, UserMerchantCreate,
VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest,
};
impl ApiEventMetric for DashboardEntryResponse {
@ -77,7 +78,10 @@ common_utils::impl_misc_api_event_type!(
BeginTotpResponse,
VerifyRecoveryCodeRequest,
VerifyTotpRequest,
RecoveryCodes
RecoveryCodes,
GetUserAuthenticationMethodsRequest,
CreateUserAuthenticationMethodRequest,
UpdateUserAuthenticationMethodRequest
);
#[cfg(feature = "dummy_connector")]

View File

@ -280,3 +280,78 @@ pub struct VerifyRecoveryCodeRequest {
pub struct RecoveryCodes {
pub recovery_codes: Vec<Secret<String>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(tag = "auth_type")]
#[serde(rename_all = "snake_case")]
pub enum AuthConfig {
OpenIdConnect {
private_config: OpenIdConnectPrivateConfig,
public_config: OpenIdConnectPublicConfig,
},
MagicLink,
Password,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct OpenIdConnectPrivateConfig {
pub base_url: String,
pub client_id: Secret<String>,
pub client_secret: Secret<String>,
pub private_key: Option<Secret<String>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct OpenIdConnectPublicConfig {
pub name: OpenIdProvider,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum OpenIdProvider {
Okta,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct OpenIdConnect {
pub name: OpenIdProvider,
pub base_url: String,
pub client_id: String,
pub client_secret: Secret<String>,
pub private_key: Option<Secret<String>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct CreateUserAuthenticationMethodRequest {
pub owner_id: String,
pub owner_type: common_enums::Owner,
pub auth_method: AuthConfig,
pub allow_signup: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UpdateUserAuthenticationMethodRequest {
pub id: String,
// TODO: When adding more fields make config and new fields option
pub auth_method: AuthConfig,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct GetUserAuthenticationMethodsRequest {
pub auth_id: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UserAuthenticationMethodResponse {
pub id: String,
pub auth_id: String,
pub auth_method: AuthMethodDetails,
pub allow_signup: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct AuthMethodDetails {
#[serde(rename = "type")]
pub auth_type: common_enums::UserAuthType,
pub name: Option<OpenIdProvider>,
}

View File

@ -2768,3 +2768,45 @@ pub enum TokenPurpose {
AcceptInvite,
UserInfo,
}
#[derive(
Clone,
Copy,
Debug,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumString,
)]
#[router_derive::diesel_enum(storage_type = "text")]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum UserAuthType {
OpenIdConnect,
MagicLink,
#[default]
Password,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumString,
)]
#[router_derive::diesel_enum(storage_type = "text")]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum Owner {
Organization,
Tenant,
Internal,
}

View File

@ -44,6 +44,7 @@ pub mod routing_algorithm;
#[allow(unused_qualifications)]
pub mod schema;
pub mod user;
pub mod user_authentication_method;
pub mod user_key_store;
pub mod user_role;

View File

@ -36,5 +36,6 @@ pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm;
pub mod user;
pub mod user_authentication_method;
pub mod user_key_store;
pub mod user_role;

View File

@ -0,0 +1,60 @@
use diesel::{associations::HasTable, ExpressionMethods};
use crate::{
query::generics, schema::user_authentication_methods::dsl, user_authentication_method::*,
PgPooledConn, StorageResult,
};
impl UserAuthenticationMethodNew {
pub async fn insert(self, conn: &PgPooledConn) -> StorageResult<UserAuthenticationMethod> {
generics::generic_insert(conn, self).await
}
}
impl UserAuthenticationMethod {
pub async fn list_user_authentication_methods_for_auth_id(
conn: &PgPooledConn,
auth_id: &str,
) -> StorageResult<Vec<Self>> {
generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
conn,
dsl::auth_id.eq(auth_id.to_owned()),
None,
None,
Some(dsl::last_modified_at.asc()),
)
.await
}
pub async fn list_user_authentication_methods_for_owner_id(
conn: &PgPooledConn,
owner_id: &str,
) -> StorageResult<Vec<Self>> {
generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
conn,
dsl::owner_id.eq(owner_id.to_owned()),
None,
None,
Some(dsl::last_modified_at.asc()),
)
.await
}
pub async fn update_user_authentication_method(
conn: &PgPooledConn,
id: &str,
user_authentication_method_update: UserAuthenticationMethodUpdate,
) -> StorageResult<Self> {
generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn,
dsl::id.eq(id.to_owned()),
OrgAuthenticationMethodUpdateInternal::from(user_authentication_method_update),
)
.await
}
}

View File

@ -1172,6 +1172,29 @@ diesel::table! {
}
}
diesel::table! {
use diesel::sql_types::*;
use crate::enums::diesel_exports::*;
user_authentication_methods (id) {
#[max_length = 64]
id -> Varchar,
#[max_length = 64]
auth_id -> Varchar,
#[max_length = 64]
owner_id -> Varchar,
#[max_length = 64]
owner_type -> Varchar,
#[max_length = 64]
auth_type -> Varchar,
private_config -> Nullable<Bytea>,
public_config -> Nullable<Jsonb>,
allow_signup -> Bool,
created_at -> Timestamp,
last_modified_at -> Timestamp,
}
}
diesel::table! {
use diesel::sql_types::*;
use crate::enums::diesel_exports::*;
@ -1270,6 +1293,7 @@ diesel::allow_tables_to_appear_in_same_query!(
reverse_lookup,
roles,
routing_algorithm,
user_authentication_methods,
user_key_store,
user_roles,
users,

View File

@ -0,0 +1,65 @@
use diesel::{AsChangeset, Identifiable, Insertable, Queryable};
use time::PrimitiveDateTime;
use crate::{encryption::Encryption, enums, schema::user_authentication_methods};
#[derive(Clone, Debug, Identifiable, Queryable)]
#[diesel(table_name = user_authentication_methods)]
pub struct UserAuthenticationMethod {
pub id: String,
pub auth_id: String,
pub owner_id: String,
pub owner_type: enums::Owner,
pub auth_type: enums::UserAuthType,
pub private_config: Option<Encryption>,
pub public_config: Option<serde_json::Value>,
pub allow_signup: bool,
pub created_at: PrimitiveDateTime,
pub last_modified_at: PrimitiveDateTime,
}
#[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
#[diesel(table_name = user_authentication_methods)]
pub struct UserAuthenticationMethodNew {
pub id: String,
pub auth_id: String,
pub owner_id: String,
pub owner_type: enums::Owner,
pub auth_type: enums::UserAuthType,
pub private_config: Option<Encryption>,
pub public_config: Option<serde_json::Value>,
pub allow_signup: bool,
pub created_at: PrimitiveDateTime,
pub last_modified_at: PrimitiveDateTime,
}
#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
#[diesel(table_name = user_authentication_methods)]
pub struct OrgAuthenticationMethodUpdateInternal {
pub private_config: Option<Encryption>,
pub public_config: Option<serde_json::Value>,
pub last_modified_at: PrimitiveDateTime,
}
pub enum UserAuthenticationMethodUpdate {
UpdateConfig {
private_config: Option<Encryption>,
public_config: Option<serde_json::Value>,
},
}
impl From<UserAuthenticationMethodUpdate> for OrgAuthenticationMethodUpdateInternal {
fn from(value: UserAuthenticationMethodUpdate) -> Self {
let last_modified_at = common_utils::date_time::now();
match value {
UserAuthenticationMethodUpdate::UpdateConfig {
private_config,
public_config,
} => Self {
private_config,
public_config,
last_modified_at,
},
}
}
}

View File

@ -220,6 +220,22 @@ impl SecretsHandler for settings::Secrets {
}
}
#[async_trait::async_trait]
impl SecretsHandler for settings::UserAuthMethodSettings {
async fn convert_to_raw_secret(
value: SecretStateContainer<Self, SecuredSecret>,
secret_management_client: &dyn SecretManagementInterface,
) -> CustomResult<SecretStateContainer<Self, RawSecret>, SecretsManagementError> {
let user_auth_methods = value.get_inner();
let encryption_key = secret_management_client
.get_secret(user_auth_methods.encryption_key.clone())
.await?;
Ok(value.transition_state(|_| Self { encryption_key }))
}
}
/// # Panics
///
/// Will panic even if kms decryption fails for at least one field
@ -302,6 +318,14 @@ pub(crate) async fn fetch_raw_secrets(
.await
.expect("Failed to decrypt payment method auth configs");
#[allow(clippy::expect_used)]
let user_auth_methods = settings::UserAuthMethodSettings::convert_to_raw_secret(
conf.user_auth_methods,
secret_management_client,
)
.await
.expect("Failed to decrypt user_auth_methods configs");
Settings {
server: conf.server,
master_database,
@ -368,5 +392,6 @@ pub(crate) async fn fetch_raw_secrets(
unmasked_headers: conf.unmasked_headers,
saved_payment_methods: conf.saved_payment_methods,
multitenancy: conf.multitenancy,
user_auth_methods,
}
}

View File

@ -123,6 +123,7 @@ pub struct Settings<S: SecretState> {
pub unmasked_headers: UnmaskedHeaders,
pub multitenancy: Multitenancy,
pub saved_payment_methods: EligiblePaymentMethods,
pub user_auth_methods: SecretStateContainer<UserAuthMethodSettings, S>,
}
#[derive(Debug, Deserialize, Clone, Default)]
@ -615,6 +616,11 @@ pub struct ConnectorRequestReferenceIdConfig {
pub merchant_ids_send_payment_id_as_connector_request_id: HashSet<String>,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct UserAuthMethodSettings {
pub encryption_key: Secret<String>,
}
impl Settings<SecuredSecret> {
pub fn new() -> ApplicationResult<Self> {
Self::with_config_path(None)

View File

@ -80,6 +80,12 @@ pub enum UserErrors {
TwoFactorAuthNotSetup,
#[error("TOTP secret not found")]
TotpSecretNotFound,
#[error("User auth method already exists")]
UserAuthMethodAlreadyExists,
#[error("Invalid user auth method operation")]
InvalidUserAuthMethodOperation,
#[error("Auth config parsing error")]
AuthConfigParsingError,
}
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
@ -204,6 +210,15 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
Self::TotpSecretNotFound => {
AER::BadRequest(ApiError::new(sub_code, 42, self.get_error_message(), None))
}
Self::UserAuthMethodAlreadyExists => {
AER::BadRequest(ApiError::new(sub_code, 43, self.get_error_message(), None))
}
Self::InvalidUserAuthMethodOperation => {
AER::BadRequest(ApiError::new(sub_code, 44, self.get_error_message(), None))
}
Self::AuthConfigParsingError => {
AER::BadRequest(ApiError::new(sub_code, 45, self.get_error_message(), None))
}
}
}
}
@ -247,6 +262,9 @@ impl UserErrors {
Self::TwoFactorAuthRequired => "Two factor auth required",
Self::TwoFactorAuthNotSetup => "Two factor auth not setup",
Self::TotpSecretNotFound => "TOTP secret not found",
Self::UserAuthMethodAlreadyExists => "User auth method already exists",
Self::InvalidUserAuthMethodOperation => "Invalid user auth method operation",
Self::AuthConfigParsingError => "Auth config parsing error",
}
}
}

View File

@ -1,11 +1,13 @@
use std::collections::HashMap;
use api_models::user::{self as user_api, InviteMultipleUserResponse};
use common_utils::ext_traits::ValueExt;
#[cfg(feature = "email")]
use diesel_models::user_role::UserRoleUpdate;
use diesel_models::{
enums::{TotpStatus, UserStatus},
user as storage_user,
user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate},
user_role::UserRoleNew,
};
use error_stack::{report, ResultExt};
@ -884,7 +886,7 @@ pub async fn resend_invite(
if e.current_context().is_db_not_found() {
e.change_context(UserErrors::InvalidRoleOperation)
.attach_printable(format!(
"User role with user_id = {} and org_id = {} is not found",
"User role with user_id = {} and merchant_id = {} is not found",
user.get_user_id(),
user_from_token.merchant_id
))
@ -1982,3 +1984,184 @@ pub async fn check_two_factor_auth_status(
},
))
}
pub async fn create_user_authentication_method(
state: SessionState,
req: user_api::CreateUserAuthenticationMethodRequest,
) -> UserResponse<()> {
let user_auth_encryption_key = hex::decode(
state
.conf
.user_auth_methods
.get_inner()
.encryption_key
.clone()
.expose(),
)
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to decode DEK")?;
let (private_config, public_config) = match req.auth_method {
user_api::AuthConfig::OpenIdConnect {
ref private_config,
ref public_config,
} => {
let private_config_value = serde_json::to_value(private_config.clone())
.change_context(UserErrors::AuthConfigParsingError)
.attach_printable("Failed to convert auth config to json")?;
let encrypted_config = domain::types::encrypt::<serde_json::Value, masking::WithType>(
private_config_value.into(),
&user_auth_encryption_key,
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to encrypt auth config")?;
Ok::<_, error_stack::Report<UserErrors>>((
Some(encrypted_config.into()),
Some(
serde_json::to_value(public_config.clone())
.change_context(UserErrors::AuthConfigParsingError)
.attach_printable("Failed to convert auth config to json")?,
),
))
}
_ => Ok((None, None)),
}?;
let auth_methods = state
.store
.list_user_authentication_methods_for_owner_id(&req.owner_id)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to get list of auth methods for the owner id")?;
let auth_id = auth_methods
.first()
.map(|auth_method| auth_method.auth_id.clone())
.unwrap_or(uuid::Uuid::new_v4().to_string());
let now = common_utils::date_time::now();
state
.store
.insert_user_authentication_method(UserAuthenticationMethodNew {
id: uuid::Uuid::new_v4().to_string(),
auth_id,
owner_id: req.owner_id,
owner_type: req.owner_type,
auth_type: req.auth_method.foreign_into(),
private_config,
public_config,
allow_signup: req.allow_signup,
created_at: now,
last_modified_at: now,
})
.await
.to_duplicate_response(UserErrors::UserAuthMethodAlreadyExists)?;
Ok(ApplicationResponse::StatusOk)
}
pub async fn update_user_authentication_method(
state: SessionState,
req: user_api::UpdateUserAuthenticationMethodRequest,
) -> UserResponse<()> {
let user_auth_encryption_key = hex::decode(
state
.conf
.user_auth_methods
.get_inner()
.encryption_key
.clone()
.expose(),
)
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to decode DEK")?;
let (private_config, public_config) = match req.auth_method {
user_api::AuthConfig::OpenIdConnect {
ref private_config,
ref public_config,
} => {
let private_config_value = serde_json::to_value(private_config.clone())
.change_context(UserErrors::AuthConfigParsingError)
.attach_printable("Failed to convert auth config to json")?;
let encrypted_config = domain::types::encrypt::<serde_json::Value, masking::WithType>(
private_config_value.into(),
&user_auth_encryption_key,
)
.await
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to encrypt auth config")?;
Ok::<_, error_stack::Report<UserErrors>>((
Some(encrypted_config.into()),
Some(
serde_json::to_value(public_config.clone())
.change_context(UserErrors::AuthConfigParsingError)
.attach_printable("Failed to convert auth config to json")?,
),
))
}
_ => Ok((None, None)),
}?;
state
.store
.update_user_authentication_method(
&req.id,
UserAuthenticationMethodUpdate::UpdateConfig {
private_config,
public_config,
},
)
.await
.change_context(UserErrors::InvalidUserAuthMethodOperation)?;
Ok(ApplicationResponse::StatusOk)
}
pub async fn list_user_authentication_methods(
state: SessionState,
req: user_api::GetUserAuthenticationMethodsRequest,
) -> UserResponse<Vec<user_api::UserAuthenticationMethodResponse>> {
let user_authentication_methods = state
.store
.list_user_authentication_methods_for_auth_id(&req.auth_id)
.await
.change_context(UserErrors::InternalServerError)?;
Ok(ApplicationResponse::Json(
user_authentication_methods
.into_iter()
.map(|auth_method| {
let auth_name = match (auth_method.auth_type, auth_method.public_config) {
(common_enums::UserAuthType::OpenIdConnect, Some(config)) => {
let open_id_public_config: user_api::OpenIdConnectPublicConfig = config
.parse_value("OpenIdConnectPublicConfig")
.change_context(UserErrors::InternalServerError)
.attach_printable("unable to parse generic data value")?;
Ok(Some(open_id_public_config.name))
}
(common_enums::UserAuthType::OpenIdConnect, None) => {
Err(UserErrors::InternalServerError)
.attach_printable("No config found for open_id_connect auth_method")
}
_ => Ok(None),
}?;
Ok(user_api::UserAuthenticationMethodResponse {
id: auth_method.id,
auth_id: auth_method.auth_id,
auth_method: user_api::AuthMethodDetails {
name: auth_name,
auth_type: auth_method.auth_type,
},
allow_signup: auth_method.allow_signup,
})
})
.collect::<UserResult<_>>()?,
))
}

View File

@ -32,6 +32,7 @@ pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm;
pub mod user;
pub mod user_authentication_method;
pub mod user_key_store;
pub mod user_role;
@ -117,6 +118,7 @@ pub trait StorageInterface:
+ user::sample_data::BatchSampleDataInterface
+ health_check::HealthCheckDbInterface
+ role::RoleInterface
+ user_authentication_method::UserAuthenticationMethodInterface
+ authentication::AuthenticationInterface
+ 'static
{

View File

@ -33,6 +33,7 @@ use super::{
dashboard_metadata::DashboardMetadataInterface,
role::RoleInterface,
user::{sample_data::BatchSampleDataInterface, UserInterface},
user_authentication_method::UserAuthenticationMethodInterface,
user_key_store::UserKeyStoreInterface,
user_role::UserRoleInterface,
};
@ -2874,3 +2875,43 @@ impl UserKeyStoreInterface for KafkaStore {
.await
}
}
#[async_trait::async_trait]
impl UserAuthenticationMethodInterface for KafkaStore {
async fn insert_user_authentication_method(
&self,
user_authentication_method: storage::UserAuthenticationMethodNew,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
self.diesel_store
.insert_user_authentication_method(user_authentication_method)
.await
}
async fn list_user_authentication_methods_for_auth_id(
&self,
auth_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError> {
self.diesel_store
.list_user_authentication_methods_for_auth_id(auth_id)
.await
}
async fn list_user_authentication_methods_for_owner_id(
&self,
owner_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError> {
self.diesel_store
.list_user_authentication_methods_for_owner_id(owner_id)
.await
}
async fn update_user_authentication_method(
&self,
id: &str,
user_authentication_method_update: storage::UserAuthenticationMethodUpdate,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
self.diesel_store
.update_user_authentication_method(id, user_authentication_method_update)
.await
}
}

View File

@ -0,0 +1,197 @@
use diesel_models::user_authentication_method::{self as storage};
use error_stack::report;
use router_env::{instrument, tracing};
use super::MockDb;
use crate::{
connection,
core::errors::{self, CustomResult},
services::Store,
};
#[async_trait::async_trait]
pub trait UserAuthenticationMethodInterface {
async fn insert_user_authentication_method(
&self,
user_authentication_method: storage::UserAuthenticationMethodNew,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError>;
async fn list_user_authentication_methods_for_auth_id(
&self,
auth_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError>;
async fn list_user_authentication_methods_for_owner_id(
&self,
owner_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError>;
async fn update_user_authentication_method(
&self,
id: &str,
user_authentication_method_update: storage::UserAuthenticationMethodUpdate,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError>;
}
#[async_trait::async_trait]
impl UserAuthenticationMethodInterface for Store {
#[instrument(skip_all)]
async fn insert_user_authentication_method(
&self,
user_authentication_method: storage::UserAuthenticationMethodNew,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
user_authentication_method
.insert(&conn)
.await
.map_err(|error| report!(errors::StorageError::from(error)))
}
#[instrument(skip_all)]
async fn list_user_authentication_methods_for_auth_id(
&self,
auth_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::UserAuthenticationMethod::list_user_authentication_methods_for_auth_id(
&conn, auth_id,
)
.await
.map_err(|error| report!(errors::StorageError::from(error)))
}
#[instrument(skip_all)]
async fn list_user_authentication_methods_for_owner_id(
&self,
owner_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::UserAuthenticationMethod::list_user_authentication_methods_for_owner_id(
&conn, owner_id,
)
.await
.map_err(|error| report!(errors::StorageError::from(error)))
}
#[instrument(skip_all)]
async fn update_user_authentication_method(
&self,
id: &str,
user_authentication_method_update: storage::UserAuthenticationMethodUpdate,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
let conn = connection::pg_connection_write(self).await?;
storage::UserAuthenticationMethod::update_user_authentication_method(
&conn,
id,
user_authentication_method_update,
)
.await
.map_err(|error| report!(errors::StorageError::from(error)))
}
}
#[async_trait::async_trait]
impl UserAuthenticationMethodInterface for MockDb {
async fn insert_user_authentication_method(
&self,
user_authentication_method: storage::UserAuthenticationMethodNew,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
let mut user_authentication_methods = self.user_authentication_methods.lock().await;
let existing_auth_id = user_authentication_methods
.iter()
.find(|uam| uam.owner_id == user_authentication_method.owner_id)
.map(|uam| uam.auth_id.clone());
let auth_id = existing_auth_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let user_authentication_method = storage::UserAuthenticationMethod {
id: uuid::Uuid::new_v4().to_string(),
auth_id,
owner_id: user_authentication_method.auth_id,
owner_type: user_authentication_method.owner_type,
auth_type: user_authentication_method.auth_type,
public_config: user_authentication_method.public_config,
private_config: user_authentication_method.private_config,
allow_signup: user_authentication_method.allow_signup,
created_at: user_authentication_method.created_at,
last_modified_at: user_authentication_method.last_modified_at,
};
user_authentication_methods.push(user_authentication_method.clone());
Ok(user_authentication_method)
}
async fn list_user_authentication_methods_for_auth_id(
&self,
auth_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError> {
let user_authentication_methods = self.user_authentication_methods.lock().await;
let user_authentication_methods_list: Vec<_> = user_authentication_methods
.iter()
.filter(|auth_method_inner| auth_method_inner.auth_id == auth_id)
.cloned()
.collect();
if user_authentication_methods_list.is_empty() {
return Err(errors::StorageError::ValueNotFound(format!(
"No user authentication method found for auth_id = {}",
auth_id
))
.into());
}
Ok(user_authentication_methods_list)
}
async fn list_user_authentication_methods_for_owner_id(
&self,
owner_id: &str,
) -> CustomResult<Vec<storage::UserAuthenticationMethod>, errors::StorageError> {
let user_authentication_methods = self.user_authentication_methods.lock().await;
let user_authentication_methods_list: Vec<_> = user_authentication_methods
.iter()
.filter(|auth_method_inner| auth_method_inner.owner_id == owner_id)
.cloned()
.collect();
if user_authentication_methods_list.is_empty() {
return Err(errors::StorageError::ValueNotFound(format!(
"No user authentication method found for owner_id = {}",
owner_id
))
.into());
}
Ok(user_authentication_methods_list)
}
async fn update_user_authentication_method(
&self,
id: &str,
user_authentication_method_update: storage::UserAuthenticationMethodUpdate,
) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
let mut user_authentication_methods = self.user_authentication_methods.lock().await;
user_authentication_methods
.iter_mut()
.find(|auth_method_inner| auth_method_inner.id == id)
.map(|auth_method_inner| {
*auth_method_inner = match user_authentication_method_update {
storage::UserAuthenticationMethodUpdate::UpdateConfig {
private_config,
public_config,
} => storage::UserAuthenticationMethod {
private_config,
public_config,
last_modified_at: common_utils::date_time::now(),
..auth_method_inner.to_owned()
},
};
auth_method_inner.to_owned()
})
.ok_or(
errors::StorageError::ValueNotFound(format!(
"No authentication method available for the id = {id}"
))
.into(),
)
}
}

View File

@ -1377,6 +1377,18 @@ impl User {
),
);
route = route.service(
web::scope("/auth")
.service(
web::resource("")
.route(web::post().to(create_user_authentication_method))
.route(web::put().to(update_user_authentication_method)),
)
.service(
web::resource("/list").route(web::get().to(list_user_authentication_methods)),
),
);
#[cfg(feature = "email")]
{
route = route

View File

@ -223,7 +223,10 @@ impl From<Flow> for ApiIdentifier {
| Flow::RecoveryCodeVerify
| Flow::RecoveryCodesGenerate
| Flow::TerminateTwoFactorAuth
| Flow::TwoFactorAuthStatus => Self::User,
| Flow::TwoFactorAuthStatus
| Flow::CreateUserAuthenticationMethod
| Flow::UpdateUserAuthenticationMethod
| Flow::ListUserAuthenticationMethods => Self::User,
Flow::ListRoles
| Flow::GetRole

View File

@ -749,3 +749,60 @@ pub async fn check_two_factor_auth_status(
))
.await
}
pub async fn create_user_authentication_method(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_api::CreateUserAuthenticationMethodRequest>,
) -> HttpResponse {
let flow = Flow::CreateUserAuthenticationMethod;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
json_payload.into_inner(),
|state, _, req_body, _| user_core::create_user_authentication_method(state, req_body),
&auth::AdminApiAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn update_user_authentication_method(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_api::UpdateUserAuthenticationMethodRequest>,
) -> HttpResponse {
let flow = Flow::UpdateUserAuthenticationMethod;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
json_payload.into_inner(),
|state, _, req_body, _| user_core::update_user_authentication_method(state, req_body),
&auth::AdminApiAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn list_user_authentication_methods(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<user_api::GetUserAuthenticationMethodsRequest>,
) -> HttpResponse {
let flow = Flow::ListUserAuthenticationMethods;
Box::pin(api::server_wrap(
flow,
state.clone(),
&req,
query.into_inner(),
|state, _, req, _| user_core::list_user_authentication_methods(state, req),
&auth::NoAuth,
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -35,6 +35,7 @@ pub mod reverse_lookup;
pub mod role;
pub mod routing_algorithm;
pub mod user;
pub mod user_authentication_method;
pub mod user_role;
use std::collections::HashMap;
@ -62,7 +63,7 @@ pub use self::{
file::*, fraud_check::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*,
merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*,
process_tracker::*, refund::*, reverse_lookup::*, role::*, routing_algorithm::*, user::*,
user_role::*,
user_authentication_method::*, user_role::*,
};
use crate::types::api::routing;

View File

@ -0,0 +1 @@
pub use diesel_models::user_authentication_method::*;

View File

@ -13,7 +13,10 @@ use crate::{
authentication::{AuthToken, UserFromToken},
authorization::roles::RoleInfo,
},
types::domain::{self, MerchantAccount, UserFromStorage},
types::{
domain::{self, MerchantAccount, UserFromStorage},
transformers::ForeignFrom,
},
};
pub mod dashboard_metadata;
@ -200,3 +203,13 @@ pub fn get_redis_connection(state: &SessionState) -> UserResult<Arc<RedisConnect
.change_context(UserErrors::InternalServerError)
.attach_printable("Failed to get redis connection")
}
impl ForeignFrom<user_api::AuthConfig> for common_enums::UserAuthType {
fn foreign_from(from: user_api::AuthConfig) -> Self {
match from {
user_api::AuthConfig::OpenIdConnect { .. } => Self::OpenIdConnect,
user_api::AuthConfig::Password => Self::Password,
user_api::AuthConfig::MagicLink => Self::MagicLink,
}
}
}

View File

@ -420,6 +420,12 @@ pub enum Flow {
TerminateTwoFactorAuth,
// Check 2FA status
TwoFactorAuthStatus,
// Create user authentication method
CreateUserAuthenticationMethod,
// Update user authentication method
UpdateUserAuthenticationMethod,
// List user authentication methods
ListUserAuthenticationMethods,
/// List initial webhook delivery attempts
WebhookEventInitialDeliveryAttemptList,
/// List delivery attempts for a webhook event

View File

@ -58,6 +58,8 @@ pub struct MockDb {
pub authentications: Arc<Mutex<Vec<store::authentication::Authentication>>>,
pub roles: Arc<Mutex<Vec<store::role::Role>>>,
pub user_key_store: Arc<Mutex<Vec<store::user_key_store::UserKeyStore>>>,
pub user_authentication_methods:
Arc<Mutex<Vec<store::user_authentication_method::UserAuthenticationMethod>>>,
}
impl MockDb {
@ -102,6 +104,7 @@ impl MockDb {
authentications: Default::default(),
roles: Default::default(),
user_key_store: Default::default(),
user_authentication_methods: Default::default(),
})
}
}

View File

@ -0,0 +1,4 @@
-- This file should undo anything in `up.sql`
DROP INDEX IF EXISTS auth_id_index;
DROP INDEX IF EXISTS owner_id_index;
DROP TABLE IF EXISTS user_authentication_methods;

View File

@ -0,0 +1,16 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS user_authentication_methods (
id VARCHAR(64) PRIMARY KEY,
auth_id VARCHAR(64) NOT NULL,
owner_id VARCHAR(64) NOT NULL,
owner_type VARCHAR(64) NOT NULL,
auth_type VARCHAR(64) NOT NULL,
private_config bytea,
public_config JSONB,
allow_signup BOOLEAN NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
last_modified_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS auth_id_index ON user_authentication_methods (auth_id);
CREATE INDEX IF NOT EXISTS owner_id_index ON user_authentication_methods (owner_id);