mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(users): Added blacklist for users (#3469)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -21,7 +21,7 @@ pub mod routes {
|
||||
routes::AppState,
|
||||
services::{
|
||||
api,
|
||||
authentication::{self as auth, AuthToken, AuthenticationData},
|
||||
authentication::{self as auth, AuthenticationData},
|
||||
authorization::permissions::Permission,
|
||||
ApplicationResponse,
|
||||
},
|
||||
@ -378,23 +378,13 @@ pub mod routes {
|
||||
req: actix_web::HttpRequest,
|
||||
json_payload: web::Json<ReportRequest>,
|
||||
) -> impl Responder {
|
||||
let state_ref = &state;
|
||||
let req_headers = &req.headers();
|
||||
|
||||
let flow = AnalyticsFlow::GenerateRefundReport;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, auth: AuthenticationData, payload| async move {
|
||||
let jwt_payload =
|
||||
auth::parse_jwt_payload::<AppState, AuthToken>(req_headers, state_ref).await;
|
||||
|
||||
let user_id = jwt_payload
|
||||
.change_context(AnalyticsError::UnknownError)?
|
||||
.user_id;
|
||||
|
||||
|state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move {
|
||||
let user = UserInterface::find_user_by_id(&*state.store, &user_id)
|
||||
.await
|
||||
.change_context(AnalyticsError::UnknownError)?;
|
||||
@ -430,23 +420,13 @@ pub mod routes {
|
||||
req: actix_web::HttpRequest,
|
||||
json_payload: web::Json<ReportRequest>,
|
||||
) -> impl Responder {
|
||||
let state_ref = &state;
|
||||
let req_headers = &req.headers();
|
||||
|
||||
let flow = AnalyticsFlow::GenerateDisputeReport;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, auth: AuthenticationData, payload| async move {
|
||||
let jwt_payload =
|
||||
auth::parse_jwt_payload::<AppState, AuthToken>(req_headers, state_ref).await;
|
||||
|
||||
let user_id = jwt_payload
|
||||
.change_context(AnalyticsError::UnknownError)?
|
||||
.user_id;
|
||||
|
||||
|state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move {
|
||||
let user = UserInterface::find_user_by_id(&*state.store, &user_id)
|
||||
.await
|
||||
.change_context(AnalyticsError::UnknownError)?;
|
||||
@ -482,23 +462,13 @@ pub mod routes {
|
||||
req: actix_web::HttpRequest,
|
||||
json_payload: web::Json<ReportRequest>,
|
||||
) -> impl Responder {
|
||||
let state_ref = &state;
|
||||
let req_headers = &req.headers();
|
||||
|
||||
let flow = AnalyticsFlow::GeneratePaymentReport;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, auth: AuthenticationData, payload| async move {
|
||||
let jwt_payload =
|
||||
auth::parse_jwt_payload::<AppState, AuthToken>(req_headers, state_ref).await;
|
||||
|
||||
let user_id = jwt_payload
|
||||
.change_context(AnalyticsError::UnknownError)?
|
||||
.user_id;
|
||||
|
||||
|state, (auth, user_id): auth::AuthenticationDataWithUserId, payload| async move {
|
||||
let user = UserInterface::find_user_by_id(&*state.store, &user_id)
|
||||
.await
|
||||
.change_context(AnalyticsError::UnknownError)?;
|
||||
|
||||
@ -66,12 +66,16 @@ pub const ROUTING_CONFIG_ID_LENGTH: usize = 10;
|
||||
pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN";
|
||||
pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes
|
||||
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days
|
||||
|
||||
pub const USER_BLACKLIST_PREFIX: &str = "BU_";
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
pub const EMAIL_TOKEN_BLACKLIST_PREFIX: &str = "BET_";
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
pub const VERIFY_CONNECTOR_ID_PREFIX: &str = "conn_verify";
|
||||
#[cfg(feature = "olap")]
|
||||
|
||||
@ -264,6 +264,11 @@ pub async fn connect_account(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn signout(state: AppState, user_from_token: auth::UserFromToken) -> UserResponse<()> {
|
||||
auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?;
|
||||
Ok(ApplicationResponse::StatusOk)
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
state: AppState,
|
||||
request: user_api::ChangePasswordRequest,
|
||||
|
||||
@ -965,6 +965,7 @@ impl User {
|
||||
web::resource("/signin").route(web::post().to(user_signin_without_invite_checks)),
|
||||
)
|
||||
.service(web::resource("/v2/signin").route(web::post().to(user_signin)))
|
||||
.service(web::resource("/signout").route(web::post().to(signout)))
|
||||
.service(web::resource("/change_password").route(web::post().to(change_password)))
|
||||
.service(web::resource("/internal_signup").route(web::post().to(internal_user_signup)))
|
||||
.service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id)))
|
||||
|
||||
@ -164,6 +164,7 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::UserSignUp
|
||||
| Flow::UserSignInWithoutInviteChecks
|
||||
| Flow::UserSignIn
|
||||
| Flow::Signout
|
||||
| Flow::ChangePassword
|
||||
| Flow::SetDashboardMetadata
|
||||
| Flow::GetMutltipleDashboardMetadata
|
||||
|
||||
@ -116,6 +116,20 @@ pub async fn user_connect_account(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn signout(state: web::Data<AppState>, http_req: HttpRequest) -> HttpResponse {
|
||||
let flow = Flow::Signout;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&http_req,
|
||||
(),
|
||||
|state, user, _| user_core::signout(state, user),
|
||||
&auth::DashboardNoPermissionAuth,
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
state: web::Data<AppState>,
|
||||
http_req: HttpRequest,
|
||||
|
||||
@ -36,6 +36,7 @@ use crate::{
|
||||
types::domain,
|
||||
utils::OptionExt,
|
||||
};
|
||||
pub mod blacklist;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AuthenticationData {
|
||||
@ -333,6 +334,9 @@ where
|
||||
state: &A,
|
||||
) -> RouterResult<(UserWithoutMerchantFromToken, AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, UserAuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
Ok((
|
||||
UserWithoutMerchantFromToken {
|
||||
@ -495,6 +499,9 @@ where
|
||||
state: &A,
|
||||
) -> RouterResult<((), AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
@ -521,6 +528,9 @@ where
|
||||
state: &A,
|
||||
) -> RouterResult<(UserFromToken, AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
@ -556,6 +566,9 @@ where
|
||||
state: &A,
|
||||
) -> RouterResult<((), AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.required_permission, permissions)?;
|
||||
@ -585,12 +598,6 @@ where
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct JwtAuthPayloadFetchMerchantAccount {
|
||||
merchant_id: String,
|
||||
role_id: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<AuthenticationData, A> for JWTAuth
|
||||
where
|
||||
@ -601,9 +608,10 @@ where
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
|
||||
let payload =
|
||||
parse_jwt_payload::<A, JwtAuthPayloadFetchMerchantAccount>(request_headers, state)
|
||||
.await?;
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
@ -638,6 +646,56 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub type AuthenticationDataWithUserId = (AuthenticationData, String);
|
||||
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<AuthenticationDataWithUserId, A> for JWTAuth
|
||||
where
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
async fn authenticate_and_fetch(
|
||||
&self,
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<(AuthenticationDataWithUserId, AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
|
||||
let key_store = state
|
||||
.store()
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
&payload.merchant_id,
|
||||
&state.store().get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InvalidJwtToken)
|
||||
.attach_printable("Failed to fetch merchant key store for the merchant id")?;
|
||||
|
||||
let merchant = state
|
||||
.store()
|
||||
.find_merchant_account_by_merchant_id(&payload.merchant_id, &key_store)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InvalidJwtToken)?;
|
||||
|
||||
let auth = AuthenticationData {
|
||||
merchant_account: merchant,
|
||||
key_store,
|
||||
};
|
||||
Ok((
|
||||
(auth.clone(), payload.user_id.clone()),
|
||||
AuthenticationType::MerchantJwt {
|
||||
merchant_id: auth.merchant_account.merchant_id.clone(),
|
||||
user_id: None,
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DashboardNoPermissionAuth;
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
@ -652,6 +710,9 @@ where
|
||||
state: &A,
|
||||
) -> RouterResult<(UserFromToken, AuthenticationType)> {
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
Ok((
|
||||
UserFromToken {
|
||||
@ -679,7 +740,10 @@ where
|
||||
request_headers: &HeaderMap,
|
||||
state: &A,
|
||||
) -> RouterResult<((), AuthenticationType)> {
|
||||
parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
|
||||
if blacklist::check_user_in_blacklist(state, &payload.user_id, payload.exp).await? {
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
Ok(((), AuthenticationType::NoAuth))
|
||||
}
|
||||
|
||||
63
crates/router/src/services/authentication/blacklist.rs
Normal file
63
crates/router/src/services/authentication/blacklist.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
use common_utils::date_time;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use redis_interface::RedisConnectionPool;
|
||||
|
||||
use crate::{
|
||||
consts::{JWT_TOKEN_TIME_IN_SECS, USER_BLACKLIST_PREFIX},
|
||||
core::errors::{ApiErrorResponse, RouterResult},
|
||||
routes::app::AppStateInfo,
|
||||
};
|
||||
#[cfg(feature = "olap")]
|
||||
use crate::{
|
||||
core::errors::{UserErrors, UserResult},
|
||||
routes::AppState,
|
||||
};
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
pub async fn insert_user_in_blacklist(state: &AppState, user_id: &str) -> UserResult<()> {
|
||||
let user_blacklist_key = format!("{}{}", USER_BLACKLIST_PREFIX, user_id);
|
||||
let expiry =
|
||||
expiry_to_i64(JWT_TOKEN_TIME_IN_SECS).change_context(UserErrors::InternalServerError)?;
|
||||
let redis_conn = get_redis_connection(state).change_context(UserErrors::InternalServerError)?;
|
||||
redis_conn
|
||||
.set_key_with_expiry(
|
||||
user_blacklist_key.as_str(),
|
||||
date_time::now_unix_timestamp(),
|
||||
expiry,
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
|
||||
pub async fn check_user_in_blacklist<A: AppStateInfo>(
|
||||
state: &A,
|
||||
user_id: &str,
|
||||
token_expiry: u64,
|
||||
) -> RouterResult<bool> {
|
||||
let token = format!("{}{}", USER_BLACKLIST_PREFIX, user_id);
|
||||
let token_issued_at = expiry_to_i64(token_expiry - JWT_TOKEN_TIME_IN_SECS)?;
|
||||
let redis_conn = get_redis_connection(state)?;
|
||||
redis_conn
|
||||
.get_key::<Option<i64>>(token.as_str())
|
||||
.await
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
.map(|timestamp| timestamp.map_or(false, |timestamp| timestamp > token_issued_at))
|
||||
}
|
||||
|
||||
fn get_redis_connection<A: AppStateInfo>(state: &A) -> RouterResult<Arc<RedisConnectionPool>> {
|
||||
state
|
||||
.store()
|
||||
.get_redis_conn()
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get redis connection")
|
||||
}
|
||||
|
||||
fn expiry_to_i64(expiry: u64) -> RouterResult<i64> {
|
||||
expiry
|
||||
.try_into()
|
||||
.into_report()
|
||||
.change_context(ApiErrorResponse::InternalServerError)
|
||||
}
|
||||
@ -285,6 +285,8 @@ pub enum Flow {
|
||||
FrmFulfillment,
|
||||
/// Change password flow
|
||||
ChangePassword,
|
||||
/// Signout flow
|
||||
Signout,
|
||||
/// Set Dashboard Metadata flow
|
||||
SetDashboardMetadata,
|
||||
/// Get Multiple Dashboard Metadata flow
|
||||
|
||||
Reference in New Issue
Block a user