Bug: feat: add blacklist for roles



After a role is updated, the users who are using that role at that time should get new tokens. So, the role should be blacklisted.

diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs
index 3c3f01dc5f9..72f160990e5 100644
--- a/crates/router/src/consts.rs
+++ b/crates/router/src/consts.rs
@@ -70,6 +70,8 @@ pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days
 
 pub const USER_BLACKLIST_PREFIX: &str = "BU_";
 
+pub const ROLE_BLACKLIST_PREFIX: &str = "BR_";
+
 #[cfg(feature = "email")]
 pub const EMAIL_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24; // 1 day
 
diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs
index 7ce72779bbb..6edbda85bfd 100644
--- a/crates/router/src/core/user_role/role.rs
+++ b/crates/router/src/core/user_role/role.rs
@@ -12,7 +12,7 @@ use crate::{
     core::errors::{StorageErrorExt, UserErrors, UserResponse},
     routes::AppState,
     services::{
-        authentication::UserFromToken,
+        authentication::{blacklist, UserFromToken},
         authorization::roles::{self, predefined_roles::PREDEFINED_ROLES},
         ApplicationResponse,
     },
@@ -219,5 +219,7 @@ pub async fn update_role(
         .await
         .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?;
 
+    blacklist::insert_role_in_blacklist(&state, role_id).await?;
+
     Ok(ApplicationResponse::StatusOk)
 }
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index 34153ef6e8f..455dc97d03e 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -13,6 +13,7 @@ use masking::ExposeInterface;
 use masking::{PeekInterface, StrongSecret};
 use serde::Serialize;
 
+use self::blacklist::BlackList;
 use super::authorization::{self, permissions::Permission};
 #[cfg(feature = "olap")]
 use super::jwt;
@@ -334,7 +335,7 @@ 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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
@@ -499,7 +500,7 @@ 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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
@@ -528,7 +529,7 @@ 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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
@@ -566,7 +567,7 @@ 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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
@@ -609,7 +610,7 @@ where
         state: &A,
     ) -> RouterResult<(AuthenticationData, 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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
@@ -659,7 +660,7 @@ where
         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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
@@ -710,7 +711,7 @@ 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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
@@ -741,7 +742,7 @@ 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? {
+        if payload.check_in_blacklist(state).await? {
             return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
         }
 
diff --git a/crates/router/src/services/authentication/blacklist.rs b/crates/router/src/services/authentication/blacklist.rs
index 325ef29bad3..346e563ee32 100644
--- a/crates/router/src/services/authentication/blacklist.rs
+++ b/crates/router/src/services/authentication/blacklist.rs
@@ -5,10 +5,11 @@ use common_utils::date_time;
 use error_stack::{IntoReport, ResultExt};
 use redis_interface::RedisConnectionPool;
 
+use super::{AuthToken, UserAuthToken};
 #[cfg(feature = "email")]
 use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS};
 use crate::{
-    consts::{JWT_TOKEN_TIME_IN_SECS, USER_BLACKLIST_PREFIX},
+    consts::{JWT_TOKEN_TIME_IN_SECS, ROLE_BLACKLIST_PREFIX, USER_BLACKLIST_PREFIX},
     core::errors::{ApiErrorResponse, RouterResult},
     routes::app::AppStateInfo,
 };
@@ -34,6 +35,22 @@ pub async fn insert_user_in_blacklist(state: &AppState, user_id: &str) -> UserRe
         .change_context(UserErrors::InternalServerError)
 }
 
+#[cfg(feature = "olap")]
+pub async fn insert_role_in_blacklist(state: &AppState, role_id: &str) -> UserResult<()> {
+    let role_blacklist_key = format!("{}{}", ROLE_BLACKLIST_PREFIX, role_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(
+            role_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,
@@ -49,6 +66,21 @@ pub async fn check_user_in_blacklist<A: AppStateInfo>(
         .map(|timestamp| timestamp.map_or(false, |timestamp| timestamp > token_issued_at))
 }
 
+pub async fn check_role_in_blacklist<A: AppStateInfo>(
+    state: &A,
+    role_id: &str,
+    token_expiry: u64,
+) -> RouterResult<bool> {
+    let token = format!("{}{}", ROLE_BLACKLIST_PREFIX, role_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))
+}
+
 #[cfg(feature = "email")]
 pub async fn insert_email_token_in_blacklist(state: &AppState, token: &str) -> UserResult<()> {
     let redis_conn = get_redis_connection(state).change_context(UserErrors::InternalServerError)?;
@@ -90,3 +122,33 @@ fn expiry_to_i64(expiry: u64) -> RouterResult<i64> {
         .into_report()
         .change_context(ApiErrorResponse::InternalServerError)
 }
+
+#[async_trait::async_trait]
+pub trait BlackList {
+    async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
+    where
+        A: AppStateInfo + Sync;
+}
+
+#[async_trait::async_trait]
+impl BlackList for AuthToken {
+    async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
+    where
+        A: AppStateInfo + Sync,
+    {
+        Ok(
+            check_user_in_blacklist(state, &self.user_id, self.exp).await?
+                || check_role_in_blacklist(state, &self.role_id, self.exp).await?,
+        )
+    }
+}
+
+#[async_trait::async_trait]
+impl BlackList for UserAuthToken {
+    async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
+    where
+        A: AppStateInfo + Sync,
+    {
+        check_user_in_blacklist(state, &self.user_id, self.exp).await
+    }
+}

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [ ] New feature
- [x] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
This PR adds ability to blacklist roles to stop JWTs after a role is changed.

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
Closes #3793 

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
Postman.
To get JWT of admin, you have to singup/create a new account. Or you can use a JWT of org_admin user.
1. Create a role
	```
	curl --location 'http://localhost:8080/user/role' \
	--header 'Content-Type: application/json' \
	--header 'Authorization: Bearer JWT of Admin' \
	--data '{
	  "role_name": "custom_role",
	  "groups": ["users_view"],
	  "role_scope": "merchant"
	}'
	```
	Response should be 200 OK
	This api will create a new custom role. This role can be assigned to people to restrict them.
2. Get the list of roles
	```
	curl --location 'http://localhost:8080/user/role/list' \
	--header 'Authorization: Bearer JWT of Admin'
	```
	In the response you will find newly created role
	```
	[
	    {
	        "role_id": "role_of8p9odNmf2puQVhQemT",
	        "permissions": [
	            "MerchantAccountRead",
	            "UsersRead"
	        ],
	        "role_name": "custom_role",
	        "role_scope": "merchant"
	    }
	]
	```
	This api is called to get the details of the role that was perviously created.
3. Update a user in the merchant in that merchant account to this newly created role
	```
	curl --location 'http://localhost:8080/user/user/invite' \
	--header 'Content-Type: application/json' \
	--header 'Authorization: Bearer JWT of Admin' \
	--data-raw '{
	    "email": "email of other user in different merchant account",
	    "name": "user name",
	    "role_id": "role_of8p9odNmf2puQVhQemT"
	}'
	```
	Response will be 200 OK.
	You can use the details you got from the previous api and invite some other person into your org, this will automatically create a new user if user doesn't exist.
4. Login as the user who got invited
	```
	curl --location 'http://localhost:8080/user/signin' \
	--header 'Content-Type: application/json' \
	--data-raw '{
	    "email": "email of user who got udpated",
	    "password": "password"
	}'
	```
	```
	{
	    "token": "JWT of user who got updated",
	    "merchant_id": "merchant_id",
	    "name": "user name",
	    "email": "email of user who got updated",
	    "verification_days_left": null,
	    "user_role": "role_of8p9odNmf2puQVhQemT"
	}
	```
	Use the credentials of the person who was invited to login and save the JWT.
5. Invite a new user with the role_id that was created previously using admin JWT
	```
	curl --location --request PUT 'http://localhost:8080/user/role/role_of8p9odNmf2puQVhQemT' \
	--header 'Content-Type: application/json' \
	--header 'Authorization: Bearer JWT of Admin' \
	--data '{
	  "groups": ["users_view", "users_manage"]
	}'
	```
	Response will be 200 OK.
	This api should be hit with Admin JWT again (not the invited user), this will update the role that we created earlier.
6. Try to use the token of the invited user which we saved earlier and hit this api and it will throw an error.
	```
	curl --location 'http://localhost:8080/user/user/invite' \
	--header 'Content-Type: application/json' \
	--header 'Authorization: Bearer JWT of invited user that was saved previously' \
	--data-raw '{
	    "email": "email of other user in different merchant account",
	    "name": "user name",
	    "role_id": "role_of8p9odNmf2puQVhQemT"
	}'
	```

	```
	{
	    "error": {
	        "type": "invalid_request",
	        "message": "Access forbidden, invalid JWT token was used",
	        "code": "IR_17"
	    }
	}
	```


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
