Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(users): Added blacklist for users #3469

Merged
merged 13 commits into from
Jan 31, 2024
38 changes: 4 additions & 34 deletions crates/router/src/analytics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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)?;
Expand Down
6 changes: 5 additions & 1 deletion crates/router/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
5 changes: 5 additions & 0 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/routes/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/routes/lock_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::UserSignUp
| Flow::UserSignInWithoutInviteChecks
| Flow::UserSignIn
| Flow::Signout
| Flow::ChangePassword
| Flow::SetDashboardMetadata
| Flow::GetMutltipleDashboardMetadata
Expand Down
14 changes: 14 additions & 0 deletions crates/router/src/routes/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
84 changes: 74 additions & 10 deletions crates/router/src/services/authentication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use crate::{
types::domain,
utils::OptionExt,
};
pub mod blacklist;

#[derive(Clone, Debug)]
pub struct AuthenticationData {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)?;
Expand All @@ -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)?;
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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
Expand All @@ -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)?;
Expand Down Expand Up @@ -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")]
Expand All @@ -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 {
Expand Down Expand Up @@ -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))
}
Expand Down
63 changes: 63 additions & 0 deletions crates/router/src/services/authentication/blacklist.rs
Original file line number Diff line number Diff line change
@@ -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)
}
Comment on lines +50 to +63
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can inline this

2 changes: 2 additions & 0 deletions crates/router_env/src/logger/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading