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:
Rachit Naithani
2024-01-31 12:49:09 +05:30
committed by GitHub
parent dfb14a34c9
commit e331d2d556
9 changed files with 169 additions and 45 deletions

View File

@ -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)?;

View File

@ -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")]

View File

@ -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,

View File

@ -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)))

View File

@ -164,6 +164,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::UserSignUp
| Flow::UserSignInWithoutInviteChecks
| Flow::UserSignIn
| Flow::Signout
| Flow::ChangePassword
| Flow::SetDashboardMetadata
| Flow::GetMutltipleDashboardMetadata

View File

@ -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,

View File

@ -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))
}

View 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)
}

View File

@ -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