feat(api_keys): add api keys route to api v2 (#5709)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Narayan Bhat
2024-08-28 18:57:36 +05:30
committed by GitHub
parent c85b4a3a27
commit 089a95069b
20 changed files with 865 additions and 426 deletions

View File

@ -2705,7 +2705,8 @@ impl MerchantConnectorAccountCreateBridge for api::MerchantConnectorCreate {
pub async fn create_connector(
state: SessionState,
req: api::MerchantConnectorCreate,
merchant_id: &id_type::MerchantId,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
) -> RouterResponse<api_models::admin::MerchantConnectorResponse> {
let store = state.store.as_ref();
let key_manager_state = &(&state).into();
@ -2716,27 +2717,13 @@ pub async fn create_connector(
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid connector name".to_string(),
})?;
let key_store = store
.get_merchant_key_store_by_merchant_id(
key_manager_state,
merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let connector_metadata = ConnectorMetadata {
connector_metadata: &req.metadata,
};
connector_metadata.validate_apple_pay_certificates_in_mca_metadata()?;
let merchant_id = merchant_account.get_id();
let merchant_account = state
.store
.find_merchant_account_by_merchant_id(key_manager_state, merchant_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
connector_metadata.validate_apple_pay_certificates_in_mca_metadata()?;
#[cfg(all(
any(feature = "v1", feature = "v2"),
@ -2956,19 +2943,14 @@ pub async fn retrieve_connector(
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
pub async fn retrieve_connector(
state: SessionState,
merchant_id: id_type::MerchantId,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
id: id_type::MerchantConnectorAccountId,
) -> RouterResponse<api_models::admin::MerchantConnectorResponse> {
let store = state.store.as_ref();
let key_manager_state = &(&state).into();
let key_store = store
.get_merchant_key_store_by_merchant_id(
key_manager_state,
&merchant_id,
&store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let merchant_id = merchant_account.get_id();
let mca = store
.find_merchant_connector_account_by_id(key_manager_state, &id, &key_store)
@ -2978,7 +2960,7 @@ pub async fn retrieve_connector(
})?;
// Validate if the merchant_id sent in the request is valid
if mca.merchant_id != merchant_id {
if mca.merchant_id != *merchant_id {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"Invalid merchant_id {} provided for merchant_connector_account {:?}",
@ -3174,19 +3156,14 @@ pub async fn delete_connector(
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
pub async fn delete_connector(
state: SessionState,
merchant_id: id_type::MerchantId,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
id: id_type::MerchantConnectorAccountId,
) -> RouterResponse<api::MerchantConnectorDeleteResponse> {
let db = state.store.as_ref();
let key_manager_state = &(&state).into();
let key_store = db
.get_merchant_key_store_by_merchant_id(
key_manager_state,
&merchant_id,
&db.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let merchant_id = merchant_account.get_id();
let mca = db
.find_merchant_connector_account_by_id(key_manager_state, &id, &key_store)
@ -3196,7 +3173,7 @@ pub async fn delete_connector(
})?;
// Validate if the merchant_id sent in the request is valid
if mca.merchant_id != merchant_id {
if mca.merchant_id != *merchant_id {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"Invalid merchant_id {} provided for merchant_connector_account {:?}",
@ -3215,7 +3192,7 @@ pub async fn delete_connector(
})?;
let response = api::MerchantConnectorDeleteResponse {
merchant_id,
merchant_id: merchant_id.clone(),
id,
deleted: is_deleted,
};
@ -3678,25 +3655,11 @@ impl BusinessProfileCreateBridge for api::BusinessProfileCreate {
pub async fn create_business_profile(
state: SessionState,
request: api::BusinessProfileCreate,
merchant_id: &id_type::MerchantId,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
) -> RouterResponse<api_models::admin::BusinessProfileResponse> {
let db = state.store.as_ref();
let key_manager_state = &(&state).into();
let key_store = db
.get_merchant_key_store_by_merchant_id(
key_manager_state,
merchant_id,
&db.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
// Get the merchant account, if few fields are not passed, then they will be inherited from
// merchant account
let merchant_account = db
.find_merchant_account_by_merchant_id(key_manager_state, merchant_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
#[cfg(all(
any(feature = "v1", feature = "v2"),
@ -3780,17 +3743,10 @@ pub async fn list_business_profile(
pub async fn retrieve_business_profile(
state: SessionState,
profile_id: id_type::ProfileId,
merchant_id: id_type::MerchantId,
key_store: domain::MerchantKeyStore,
) -> RouterResponse<api_models::admin::BusinessProfileResponse> {
let db = state.store.as_ref();
let key_store = db
.get_merchant_key_store_by_merchant_id(
&(&state).into(),
&merchant_id,
&db.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let business_profile = db
.find_business_profile_by_profile_id(&(&state).into(), &key_store, &profile_id)
.await
@ -4042,19 +3998,10 @@ impl BusinessProfileUpdateBridge for api::BusinessProfileUpdate {
pub async fn update_business_profile(
state: SessionState,
profile_id: &id_type::ProfileId,
merchant_id: &id_type::MerchantId,
key_store: domain::MerchantKeyStore,
request: api::BusinessProfileUpdate,
) -> RouterResponse<api::BusinessProfileResponse> {
let db = state.store.as_ref();
let key_store = db
.get_merchant_key_store_by_merchant_id(
&(&state).into(),
merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
.attach_printable("Error while fetching the key store by merchant_id")?;
let key_manager_state = &(&state).into();
let business_profile = db
@ -4064,12 +4011,6 @@ pub async fn update_business_profile(
id: profile_id.get_string_repr().to_owned(),
})?;
if business_profile.merchant_id != *merchant_id {
Err(errors::ApiErrorResponse::AccessForbidden {
resource: profile_id.get_string_repr().to_owned(),
})?
}
let business_profile_update = request
.get_update_business_profile_object(&state, &key_store)
.await?;

View File

@ -9,6 +9,7 @@ use crate::{
configs::settings,
consts,
core::errors::{self, RouterResponse, StorageErrorExt},
db::domain,
routes::{metrics, SessionState},
services::{authentication, ApplicationResponse},
types::{api, storage, transformers::ForeignInto},
@ -112,22 +113,12 @@ impl PlaintextApiKey {
pub async fn create_api_key(
state: SessionState,
api_key: api::CreateApiKeyRequest,
merchant_id: common_utils::id_type::MerchantId,
key_store: domain::MerchantKeyStore,
) -> RouterResponse<api::CreateApiKeyResponse> {
let api_key_config = state.conf.api_keys.get_inner();
let store = state.store.as_ref();
// We are not fetching merchant account as the merchant key store is needed to search for a
// merchant account.
// Instead, we're only fetching merchant key store, as it is sufficient to identify
// non-existence of a merchant account.
store
.get_merchant_key_store_by_merchant_id(
&(&state).into(),
&merchant_id,
&store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let merchant_id = key_store.merchant_id.clone();
let hash_key = api_key_config.get_hash_key()?;
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
@ -266,12 +257,12 @@ pub async fn add_api_key_expiry_task(
#[instrument(skip_all)]
pub async fn retrieve_api_key(
state: SessionState,
merchant_id: &common_utils::id_type::MerchantId,
merchant_id: common_utils::id_type::MerchantId,
key_id: &str,
) -> RouterResponse<api::RetrieveApiKeyResponse> {
let store = state.store.as_ref();
let api_key = store
.find_api_key_by_merchant_id_key_id_optional(merchant_id, key_id)
.find_api_key_by_merchant_id_key_id_optional(&merchant_id, key_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
.attach_printable("Failed to retrieve API key")?

View File

@ -1,13 +1,7 @@
use actix_web::{web, HttpRequest, HttpResponse};
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
use error_stack::ResultExt;
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
use hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse;
use router_env::{instrument, tracing, Flow};
use super::app::AppState;
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
use crate::headers;
use crate::{
core::{admin::*, api_locking},
services::{api, authentication as auth, authorization::permissions::Permission},
@ -102,18 +96,6 @@ pub async fn merchant_account_create(
/// Merchant Account - Retrieve
///
/// Retrieve a merchant account details.
#[utoipa::path(
get,
path = "/accounts/{account_id}",
params (("account_id" = String, Path, description = "The unique identifier for the merchant account")),
responses(
(status = 200, description = "Merchant Account Retrieved", body = MerchantAccountResponse),
(status = 404, description = "Merchant account not found")
),
tag = "Merchant Account",
operation_id = "Retrieve a Merchant Account",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::MerchantsAccountRetrieve))]
pub async fn retrieve_merchant_account(
state: web::Data<AppState>,
@ -168,19 +150,6 @@ pub async fn merchant_account_list(
/// Merchant Account - Update
///
/// To update an existing merchant account. Helpful in updating merchant details such as email, contact details, or other configuration details like webhook, routing algorithm etc
#[utoipa::path(
post,
path = "/accounts/{account_id}",
request_body = MerchantAccountUpdate,
params (("account_id" = String, Path, description = "The unique identifier for the merchant account")),
responses(
(status = 200, description = "Merchant Account Updated", body = MerchantAccountResponse),
(status = 404, description = "Merchant account not found")
),
tag = "Merchant Account",
operation_id = "Update a Merchant Account",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::MerchantsAccountUpdate))]
pub async fn update_merchant_account(
state: web::Data<AppState>,
@ -212,20 +181,7 @@ pub async fn update_merchant_account(
/// Merchant Account - Delete
///
/// To delete a merchant account
#[utoipa::path(
delete,
path = "/accounts/{account_id}",
params (("account_id" = String, Path, description = "The unique identifier for the merchant account")),
responses(
(status = 200, description = "Merchant Account Deleted", body = MerchantAccountDeleteResponse),
(status = 404, description = "Merchant account not found")
),
tag = "Merchant Account",
operation_id = "Delete a Merchant Account",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::MerchantsAccountDelete))]
// #[delete("/{id}")]
pub async fn delete_merchant_account(
state: web::Data<AppState>,
req: HttpRequest,
@ -247,52 +203,6 @@ pub async fn delete_merchant_account(
.await
}
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
struct HeaderMapStruct<'a> {
headers: &'a actix_http::header::HeaderMap,
}
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
impl<'a> HeaderMapStruct<'a> {
pub fn from(request: &'a HttpRequest) -> Self {
HeaderMapStruct {
headers: request.headers(),
}
}
fn get_mandatory_header_value_by_key(
&self,
key: String,
) -> Result<&str, error_stack::Report<ApiErrorResponse>> {
self.headers
.get(&key)
.ok_or(ApiErrorResponse::InvalidRequestData {
message: format!("Missing header key: {}", key),
})
.attach_printable(format!("Failed to find header key: {}", key))?
.to_str()
.change_context(ApiErrorResponse::InternalServerError)
.attach_printable(format!(
"Failed to convert header value to string for header key: {}",
key
))
}
pub fn get_merchant_id_from_header(
&self,
) -> crate::errors::RouterResult<common_utils::id_type::MerchantId> {
self.get_mandatory_header_value_by_key(headers::X_MERCHANT_ID.into())
.map(|val| val.to_owned())
.and_then(|merchant_id| {
common_utils::id_type::MerchantId::wrap(merchant_id)
.change_context(ApiErrorResponse::InternalServerError)
.attach_printable(
"Error while converting MerchantId from `x-merchant-id` string header",
)
})
}
}
/// Merchant Connector - Create
///
/// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc."
@ -300,18 +210,6 @@ impl<'a> HeaderMapStruct<'a> {
any(feature = "v1", feature = "v2"),
not(feature = "merchant_connector_account_v2")
))]
#[utoipa::path(
post,
path = "/accounts/{account_id}/connectors",
request_body = MerchantConnectorCreate,
responses(
(status = 200, description = "Merchant Connector Created", body = MerchantConnectorResponse),
(status = 400, description = "Missing Mandatory fields"),
),
tag = "Merchant Connector Account",
operation_id = "Create a Merchant Connector",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::MerchantConnectorsCreate))]
pub async fn connector_create(
state: web::Data<AppState>,
@ -327,9 +225,11 @@ pub async fn connector_create(
state,
&req,
payload,
|state, _, req, _| create_connector(state, req, &merchant_id),
|state, auth_data, req, _| {
create_connector(state, req, auth_data.merchant_account, auth_data.key_store)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()),
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
required_permission: Permission::MerchantConnectorAccountWrite,
@ -344,18 +244,6 @@ pub async fn connector_create(
///
/// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc."
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
#[utoipa::path(
post,
path = "/connector_accounts",
request_body = MerchantConnectorCreate,
responses(
(status = 200, description = "Merchant Connector Created", body = MerchantConnectorResponse),
(status = 400, description = "Missing Mandatory fields"),
),
tag = "Merchant Connector Account",
operation_id = "Create a Merchant Connector",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::MerchantConnectorsCreate))]
pub async fn connector_create(
state: web::Data<AppState>,
@ -364,17 +252,17 @@ pub async fn connector_create(
) -> HttpResponse {
let flow = Flow::MerchantConnectorsCreate;
let payload = json_payload.into_inner();
let merchant_id = payload.merchant_id.clone();
Box::pin(api::server_wrap(
flow,
state,
&req,
payload,
|state, _, req, _| create_connector(state, req, &merchant_id),
|state, auth_data, req, _| {
create_connector(state, req, auth_data.merchant_account, auth_data.key_store)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::MerchantConnectorAccountWrite,
},
req.headers(),
@ -437,7 +325,7 @@ pub async fn connector_retrieve(
)
},
auth::auth_type(
&auth::AdminApiAuthWithMerchantId::default(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromRoute {
merchant_id,
required_permission: Permission::MerchantConnectorAccountRead,
@ -452,21 +340,6 @@ pub async fn connector_retrieve(
///
/// Retrieve Merchant Connector Details
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
#[utoipa::path(
get,
path = "/connector_accounts/{id}",
params(
("id" = i32, Path, description = "The unique identifier for the Merchant Connector")
),
responses(
(status = 200, description = "Merchant Connector retrieved successfully", body = MerchantConnectorResponse),
(status = 404, description = "Merchant Connector does not exist in records"),
(status = 401, description = "Unauthorized request")
),
tag = "Merchant Connector Account",
operation_id = "Retrieve a Merchant Connector",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::MerchantConnectorsRetrieve))]
pub async fn connector_retrieve(
state: web::Data<AppState>,
@ -477,23 +350,22 @@ pub async fn connector_retrieve(
let id = path.into_inner();
let payload = web::Json(admin::MerchantConnectorId { id: id.clone() }).into_inner();
let merchant_id = match HeaderMapStruct::from(&req).get_merchant_id_from_header() {
Ok(val) => val,
Err(err) => {
return api::log_and_return_error_response(err);
}
};
api::server_wrap(
flow,
state,
&req,
payload,
|state, _, req, _| retrieve_connector(state, merchant_id.clone(), req.id.clone()),
|state, auth_data, req, _| {
retrieve_connector(
state,
auth_data.merchant_account,
auth_data.key_store,
req.id.clone(),
)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::MerchantConnectorAccountRead,
},
req.headers(),
@ -536,7 +408,7 @@ pub async fn payment_connector_list(
merchant_id.to_owned(),
|state, _auth, merchant_id, _| list_payment_connectors(state, merchant_id, None),
auth::auth_type(
&auth::AdminApiAuthWithMerchantId::default(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromRoute {
merchant_id,
required_permission: Permission::MerchantConnectorAccountRead,
@ -587,7 +459,7 @@ pub async fn payment_connector_list_profile(
)
},
auth::auth_type(
&auth::AdminApiAuthWithMerchantId::default(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromRoute {
merchant_id,
required_permission: Permission::MerchantConnectorAccountRead,
@ -650,7 +522,7 @@ pub async fn connector_update(
)
},
auth::auth_type(
&auth::AdminApiAuthWithMerchantId::default(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
required_permission: Permission::MerchantConnectorAccountWrite,
@ -774,21 +646,6 @@ pub async fn connector_delete(
///
/// Delete or Detach a Merchant Connector from Merchant Account
#[cfg(all(feature = "v2", feature = "merchant_connector_account_v2"))]
#[utoipa::path(
delete,
path = "/connector_accounts/{id}",
params(
("id" = i32, Path, description = "The unique identifier for the Merchant Connector")
),
responses(
(status = 200, description = "Merchant Connector Deleted", body = MerchantConnectorDeleteResponse),
(status = 404, description = "Merchant Connector does not exist in records"),
(status = 401, description = "Unauthorized request")
),
tag = "Merchant Connector Account",
operation_id = "Delete a Merchant Connector",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::MerchantConnectorsDelete))]
pub async fn connector_delete(
state: web::Data<AppState>,
@ -799,25 +656,22 @@ pub async fn connector_delete(
let id = path.into_inner();
let payload = web::Json(admin::MerchantConnectorId { id: id.clone() }).into_inner();
let header_map = HeaderMapStruct {
headers: req.headers(),
};
let merchant_id = match header_map.get_merchant_id_from_header() {
Ok(val) => val,
Err(err) => {
return api::log_and_return_error_response(err);
}
};
Box::pin(api::server_wrap(
flow,
state,
&req,
payload,
|state, _, req, _| delete_connector(state, merchant_id.clone(), req.id),
|state, auth_data, req, _| {
delete_connector(
state,
auth_data.merchant_account,
auth_data.key_store,
req.id,
)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::MerchantConnectorAccountWrite,
},
req.headers(),
@ -897,11 +751,13 @@ pub async fn business_profile_create(
state,
&req,
payload,
|state, _, req, _| create_business_profile(state, req, &merchant_id),
|state, auth_data, req, _| {
create_business_profile(state, req, auth_data.merchant_account, auth_data.key_store)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()),
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
merchant_id,
required_permission: Permission::MerchantAccountWrite,
},
req.headers(),
@ -921,23 +777,17 @@ pub async fn business_profile_create(
let flow = Flow::BusinessProfileCreate;
let payload = json_payload.into_inner();
let merchant_id = match HeaderMapStruct::from(&req).get_merchant_id_from_header() {
Ok(val) => val,
Err(err) => {
return api::log_and_return_error_response(err);
}
};
Box::pin(api::server_wrap(
flow,
state,
&req,
payload,
|state, _, req, _| create_business_profile(state, req, &merchant_id),
|state, auth_data, req, _| {
create_business_profile(state, req, auth_data.merchant_account, auth_data.key_store)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::MerchantAccountWrite,
},
req.headers(),
@ -968,9 +818,11 @@ pub async fn business_profile_retrieve(
state,
&req,
profile_id,
|state, _, profile_id, _| retrieve_business_profile(state, profile_id, merchant_id.clone()),
|state, auth_data, profile_id, _| {
retrieve_business_profile(state, profile_id, auth_data.key_store)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()),
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
required_permission: Permission::MerchantAccountRead,
@ -992,23 +844,17 @@ pub async fn business_profile_retrieve(
let flow = Flow::BusinessProfileRetrieve;
let profile_id = path.into_inner();
let merchant_id = match HeaderMapStruct::from(&req).get_merchant_id_from_header() {
Ok(val) => val,
Err(err) => {
return api::log_and_return_error_response(err);
}
};
Box::pin(api::server_wrap(
flow,
state,
&req,
profile_id,
|state, _, profile_id, _| retrieve_business_profile(state, profile_id, merchant_id.clone()),
|state, auth_data, profile_id, _| {
retrieve_business_profile(state, profile_id, auth_data.key_store)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::MerchantAccountRead,
},
req.headers(),
@ -1041,9 +887,11 @@ pub async fn business_profile_update(
state,
&req,
json_payload.into_inner(),
|state, _, req, _| update_business_profile(state, &profile_id, &merchant_id, req),
|state, auth_data, req, _| {
update_business_profile(state, &profile_id, auth_data.key_store, req)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()),
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
required_permission: Permission::MerchantAccountWrite,
@ -1066,23 +914,17 @@ pub async fn business_profile_update(
let flow = Flow::BusinessProfileUpdate;
let profile_id = path.into_inner();
let merchant_id = match HeaderMapStruct::from(&req).get_merchant_id_from_header() {
Ok(val) => val,
Err(err) => {
return api::log_and_return_error_response(err);
}
};
Box::pin(api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
|state, _, req, _| update_business_profile(state, &profile_id, &merchant_id, req),
|state, auth_data, req, _| {
update_business_profile(state, &profile_id, auth_data.key_store, req)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::MerchantAccountWrite,
},
req.headers(),

View File

@ -12,19 +12,10 @@ use crate::{
///
/// Create a new API Key for accessing our APIs from your servers. The plaintext API Key will be
/// displayed only once on creation, so ensure you store it securely.
#[utoipa::path(
post,
path = "/api_keys/{merchant_id)",
params(("merchant_id" = String, Path, description = "The unique identifier for the merchant account")),
request_body= CreateApiKeyRequest,
responses(
(status = 200, description = "API Key created", body = CreateApiKeyResponse),
(status = 400, description = "Invalid data")
),
tag = "API Key",
operation_id = "Create an API Key",
security(("admin_api_key" = []))
)]
#[cfg(all(
any(feature = "v1", feature = "v2"),
not(feature = "merchant_account_v2")
))]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyCreate))]
pub async fn api_key_create(
state: web::Data<AppState>,
@ -41,11 +32,11 @@ pub async fn api_key_create(
state,
&req,
payload,
|state, _, payload, _| async {
api_keys::create_api_key(state, payload, merchant_id.clone()).await
|state, auth_data, payload, _| async {
api_keys::create_api_key(state, payload, auth_data.key_store).await
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::AdminApiAuthWithMerchantIdFromRoute(merchant_id.clone()),
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
required_permission: Permission::ApiKeyWrite,
@ -56,24 +47,81 @@ pub async fn api_key_create(
))
.await
}
#[cfg(all(feature = "v2", feature = "merchant_account_v2"))]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyCreate))]
pub async fn api_key_create(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<api_types::CreateApiKeyRequest>,
) -> impl Responder {
let flow = Flow::ApiKeyCreate;
let payload = json_payload.into_inner();
Box::pin(api::server_wrap(
flow,
state,
&req,
payload,
|state, auth_data, payload, _| async {
api_keys::create_api_key(state, payload, auth_data.key_store).await
},
auth::auth_type(
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::ApiKeyWrite,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
))
.await
}
/// API Key - Retrieve
///
/// Retrieve information about the specified API Key.
#[cfg(all(feature = "v2", feature = "merchant_account_v2"))]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRetrieve))]
pub async fn api_key_retrieve(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<String>,
) -> impl Responder {
let flow = Flow::ApiKeyRetrieve;
let key_id = path.into_inner();
api::server_wrap(
flow,
state,
&req,
&key_id,
|state, auth_data, key_id, _| {
api_keys::retrieve_api_key(
state,
auth_data.merchant_account.get_id().to_owned(),
key_id,
)
},
auth::auth_type(
&auth::AdminApiAuthWithMerchantIdFromHeader,
&auth::JWTAuthMerchantFromHeader {
required_permission: Permission::ApiKeyRead,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
)
.await
}
#[cfg(all(
any(feature = "v1", feature = "v2"),
not(feature = "merchant_account_v2")
))]
/// API Key - Retrieve
///
/// Retrieve information about the specified API Key.
#[utoipa::path(
get,
path = "/api_keys/{merchant_id}/{key_id}",
params (
("merchant_id" = String, Path, description = "The unique identifier for the merchant account"),
("key_id" = String, Path, description = "The unique identifier for the API Key")
),
responses(
(status = 200, description = "API Key retrieved", body = RetrieveApiKeyResponse),
(status = 404, description = "API Key not found")
),
tag = "API Key",
operation_id = "Retrieve an API Key",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRetrieve))]
pub async fn api_key_retrieve(
state: web::Data<AppState>,
@ -87,7 +135,7 @@ pub async fn api_key_retrieve(
flow,
state,
&req,
(&merchant_id, &key_id),
(merchant_id.clone(), &key_id),
|state, _, (merchant_id, key_id), _| api_keys::retrieve_api_key(state, merchant_id, key_id),
auth::auth_type(
&auth::AdminApiAuth,
@ -101,25 +149,14 @@ pub async fn api_key_retrieve(
)
.await
}
#[cfg(all(
any(feature = "v1", feature = "v2"),
not(feature = "merchant_account_v2")
))]
/// API Key - Update
///
/// Update information for the specified API Key.
#[utoipa::path(
post,
path = "/api_keys/{merchant_id}/{key_id}",
request_body = UpdateApiKeyRequest,
params (
("merchant_id" = String, Path, description = "The unique identifier for the merchant account"),
("key_id" = String, Path, description = "The unique identifier for the API Key")
),
responses(
(status = 200, description = "API Key updated", body = RetrieveApiKeyResponse),
(status = 404, description = "API Key not found")
),
tag = "API Key",
operation_id = "Update an API Key",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyUpdate))]
pub async fn api_key_update(
state: web::Data<AppState>,
@ -151,25 +188,47 @@ pub async fn api_key_update(
)
.await
}
#[cfg(all(feature = "v2", feature = "merchant_account_v2"))]
pub async fn api_key_update(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<(common_utils::id_type::MerchantId, String)>,
json_payload: web::Json<api_types::UpdateApiKeyRequest>,
) -> impl Responder {
let flow = Flow::ApiKeyUpdate;
let (merchant_id, key_id) = path.into_inner();
let mut payload = json_payload.into_inner();
payload.key_id = key_id;
payload.merchant_id.clone_from(&merchant_id);
api::server_wrap(
flow,
state,
&req,
payload,
|state, _, payload, _| api_keys::update_api_key(state, payload),
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id,
required_permission: Permission::ApiKeyWrite,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
)
.await
}
#[cfg(all(
any(feature = "v1", feature = "v2"),
not(feature = "merchant_account_v2")
))]
/// API Key - Revoke
///
/// Revoke the specified API Key. Once revoked, the API Key can no longer be used for
/// authenticating with our APIs.
#[utoipa::path(
delete,
path = "/api_keys/{merchant_id)/{key_id}",
params (
("merchant_id" = String, Path, description = "The unique identifier for the merchant account"),
("key_id" = String, Path, description = "The unique identifier for the API Key")
),
responses(
(status = 200, description = "API Key revoked", body = RevokeApiKeyResponse),
(status = 404, description = "API Key not found")
),
tag = "API Key",
operation_id = "Revoke an API Key",
security(("admin_api_key" = []))
)]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRevoke))]
pub async fn api_key_revoke(
state: web::Data<AppState>,
@ -197,6 +256,36 @@ pub async fn api_key_revoke(
)
.await
}
#[cfg(all(feature = "v2", feature = "merchant_account_v2"))]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRevoke))]
pub async fn api_key_revoke(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<(common_utils::id_type::MerchantId, String)>,
) -> impl Responder {
let flow = Flow::ApiKeyRevoke;
let (merchant_id, key_id) = path.into_inner();
api::server_wrap(
flow,
state,
&req,
(&merchant_id, &key_id),
|state, _, (merchant_id, key_id), _| api_keys::revoke_api_key(state, merchant_id, key_id),
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
required_permission: Permission::ApiKeyWrite,
},
req.headers(),
),
api_locking::LockAction::NotApplicable,
)
.await
}
/// API Key - List
///
/// List all API Keys associated with your merchant account.

View File

@ -1405,7 +1405,27 @@ impl Poll {
pub struct ApiKeys;
#[cfg(feature = "olap")]
#[cfg(all(feature = "v2", feature = "olap", feature = "merchant_account_v2"))]
impl ApiKeys {
pub fn server(state: AppState) -> Scope {
web::scope("/v2/api_keys")
.app_data(web::Data::new(state))
.service(web::resource("").route(web::post().to(api_key_create)))
.service(web::resource("/list").route(web::get().to(api_key_list)))
.service(
web::resource("/{key_id}")
.route(web::get().to(api_key_retrieve))
.route(web::put().to(api_key_update))
.route(web::delete().to(api_key_revoke)),
)
}
}
#[cfg(all(
feature = "olap",
any(feature = "v1", feature = "v2"),
not(feature = "merchant_account_v2")
))]
impl ApiKeys {
pub fn server(state: AppState) -> Scope {
web::scope("/api_keys/{merchant_id}")

View File

@ -1598,7 +1598,7 @@ pub async fn payments_manual_update(
&req,
payload,
|state, _auth, req, _req_state| payments::payments_manual_update(state, req),
&auth::AdminApiAuthWithMerchantId::default(),
&auth::AdminApiAuthWithMerchantIdFromHeader,
locking_action,
))
.await

View File

@ -692,11 +692,11 @@ where
}
}
#[derive(Debug, Default)]
pub struct AdminApiAuthWithMerchantId(AdminApiAuth);
#[derive(Debug)]
pub struct AdminApiAuthWithMerchantIdFromRoute(pub id_type::MerchantId);
#[async_trait]
impl<A> AuthenticateAndFetch<AuthenticationData, A> for AdminApiAuthWithMerchantId
impl<A> AuthenticateAndFetch<AuthenticationData, A> for AdminApiAuthWithMerchantIdFromRoute
where
A: SessionStateInfo + Sync,
{
@ -705,25 +705,12 @@ where
request_headers: &HeaderMap,
state: &A,
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
self.0
AdminApiAuth
.authenticate_and_fetch(request_headers, state)
.await?;
let merchant_id =
get_header_value_by_key(headers::X_MERCHANT_ID.to_string(), request_headers)?
.get_required_value(headers::X_MERCHANT_ID)
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: format!("`{}` header is missing", headers::X_MERCHANT_ID),
})
.and_then(|merchant_id_str| {
id_type::MerchantId::try_from(std::borrow::Cow::from(
merchant_id_str.to_string(),
))
.change_context(
errors::ApiErrorResponse::InvalidRequestData {
message: format!("`{}` header is invalid", headers::X_MERCHANT_ID),
},
)
})?;
let merchant_id = self.0.clone();
let key_manager_state = &(&state.session_state()).into();
let key_store = state
.store()
@ -755,6 +742,113 @@ where
}
})?;
let auth = AuthenticationData {
merchant_account: merchant,
key_store,
profile_id: None,
};
Ok((
auth,
AuthenticationType::AdminApiAuthWithMerchantId { merchant_id },
))
}
}
/// A helper struct to extract headers from the request
struct HeaderMapStruct<'a> {
headers: &'a HeaderMap,
}
impl<'a> HeaderMapStruct<'a> {
pub fn new(headers: &'a HeaderMap) -> Self {
HeaderMapStruct { headers }
}
fn get_mandatory_header_value_by_key(
&self,
key: String,
) -> Result<&str, error_stack::Report<errors::ApiErrorResponse>> {
self.headers
.get(&key)
.ok_or(errors::ApiErrorResponse::InvalidRequestData {
message: format!("Missing header key: `{}`", key),
})
.attach_printable(format!("Failed to find header key: {}", key))?
.to_str()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "`X-Merchant-Id` in headers",
})
.attach_printable(format!(
"Failed to convert header value to string for header key: {}",
key
))
}
pub fn get_merchant_id_from_header(&self) -> RouterResult<id_type::MerchantId> {
self.get_mandatory_header_value_by_key(headers::X_MERCHANT_ID.into())
.map(|val| val.to_owned())
.and_then(|merchant_id| {
id_type::MerchantId::wrap(merchant_id).change_context(
errors::ApiErrorResponse::InvalidRequestData {
message: format!("`{}` header is invalid", headers::X_MERCHANT_ID),
},
)
})
}
}
/// Get the merchant-id from `x-merchant-id` header
#[derive(Debug)]
pub struct AdminApiAuthWithMerchantIdFromHeader;
#[async_trait]
impl<A> AuthenticateAndFetch<AuthenticationData, A> for AdminApiAuthWithMerchantIdFromHeader
where
A: SessionStateInfo + Sync,
{
async fn authenticate_and_fetch(
&self,
request_headers: &HeaderMap,
state: &A,
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
AdminApiAuth
.authenticate_and_fetch(request_headers, state)
.await?;
let merchant_id = HeaderMapStruct::new(request_headers).get_merchant_id_from_header()?;
let key_manager_state = &(&state.session_state()).into();
let key_store = state
.store()
.get_merchant_key_store_by_merchant_id(
key_manager_state,
&merchant_id,
&state.store().get_master_key().to_vec().into(),
)
.await
.map_err(|e| {
if e.current_context().is_db_not_found() {
e.change_context(errors::ApiErrorResponse::MerchantAccountNotFound)
} else {
e.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch merchant key store for the merchant id")
}
})?;
let merchant = state
.store()
.find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store)
.await
.map_err(|e| {
if e.current_context().is_db_not_found() {
e.change_context(errors::ApiErrorResponse::Unauthorized)
} else {
e.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to fetch merchant account for the merchant id")
}
})?;
let auth = AuthenticationData {
merchant_account: merchant,
key_store,
@ -1026,6 +1120,111 @@ pub struct JWTAuthMerchantFromRoute {
pub required_permission: Permission,
}
pub struct JWTAuthMerchantFromHeader {
pub required_permission: Permission,
}
#[async_trait]
impl<A> AuthenticateAndFetch<(), A> for JWTAuthMerchantFromHeader
where
A: SessionStateInfo + Sync,
{
async fn authenticate_and_fetch(
&self,
request_headers: &HeaderMap,
state: &A,
) -> RouterResult<((), AuthenticationType)> {
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
if payload.check_in_blacklist(state).await? {
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
}
let permissions = authorization::get_permissions(state, &payload).await?;
authorization::check_authorization(&self.required_permission, &permissions)?;
let merchant_id_from_header =
HeaderMapStruct::new(request_headers).get_merchant_id_from_header()?;
// Check if token has access to MerchantId that has been requested through headers
if payload.merchant_id != merchant_id_from_header {
return Err(report!(errors::ApiErrorResponse::InvalidJwtToken));
}
Ok((
(),
AuthenticationType::MerchantJwt {
merchant_id: payload.merchant_id,
user_id: Some(payload.user_id),
},
))
}
}
#[async_trait]
impl<A> AuthenticateAndFetch<AuthenticationData, A> for JWTAuthMerchantFromHeader
where
A: SessionStateInfo + Sync,
{
async fn authenticate_and_fetch(
&self,
request_headers: &HeaderMap,
state: &A,
) -> RouterResult<(AuthenticationData, AuthenticationType)> {
let payload = parse_jwt_payload::<A, AuthToken>(request_headers, state).await?;
if payload.check_in_blacklist(state).await? {
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
}
let permissions = authorization::get_permissions(state, &payload).await?;
authorization::check_authorization(&self.required_permission, &permissions)?;
let merchant_id_from_header =
HeaderMapStruct::new(request_headers).get_merchant_id_from_header()?;
// Check if token has access to MerchantId that has been requested through headers
if payload.merchant_id != merchant_id_from_header {
return Err(report!(errors::ApiErrorResponse::InvalidJwtToken));
}
let key_manager_state = &(&state.session_state()).into();
let key_store = state
.store()
.get_merchant_key_store_by_merchant_id(
key_manager_state,
&payload.merchant_id,
&state.store().get_master_key().to_vec().into(),
)
.await
.to_not_found_response(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(
key_manager_state,
&payload.merchant_id,
&key_store,
)
.await
.to_not_found_response(errors::ApiErrorResponse::InvalidJwtToken)
.attach_printable("Failed to fetch merchant account for the merchant id")?;
let auth = AuthenticationData {
merchant_account: merchant,
key_store,
profile_id: payload.profile_id,
};
Ok((
auth,
AuthenticationType::MerchantJwt {
merchant_id: payload.merchant_id,
user_id: Some(payload.user_id),
},
))
}
}
#[async_trait]
impl<A> AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute
where