diff --git a/api-reference-v2/README.md b/api-reference-v2/README.md new file mode 100644 index 000000000000..61bc1f08938b --- /dev/null +++ b/api-reference-v2/README.md @@ -0,0 +1,45 @@ +# Api Reference + +We use the [openapi specification](https://swagger.io/specification) for the api reference. The openapi file is generated from the code base [openapi_spec.json](openapi_spec.json). + +## How to generate the file + +This file is automatically generated from our CI pipeline when the PR is raised. However if you want to generate it manually, the following command can be used + +```bash +cargo r -p openapi --features v2 +``` + +## Render the generated openapi spec file + +In order to render the openapi spec file, we use a tool called [mintlify](https://mintlify.com/). Local setup instructions can be found [here](https://mintlify.com/docs/development#development). Once the cli is installed, Run the following command to render the openapi spec + +- Navigate to the directory where `mint.json` exists + +```bash +cd api-reference-v2 +``` + +- Run the cli + +```bash +mintlify dev +``` + +## Add new routes to openapi + +If you have added new routes to the openapi. Then in order for them to be displayed on the mintlify, run the following commands + +- Switch to the directory where api reference ( mint.json ) exists + +```bash +cd api-reference-v2 +``` + +- Run the following command to generate the route files + +```bash +npx @mintlify/scraping@latest openapi-file openapi_spec.json -o api-reference +``` + +This will generate files in [api-reference](api-reference) folder. These routes should be added to the [mint.json](mint.json) file under navigation, under respective group. diff --git a/api-reference-v2/api-reference/api-key/api-key--create.mdx b/api-reference-v2/api-reference/api-key/api-key--create.mdx new file mode 100644 index 000000000000..a92a8ea77fd3 --- /dev/null +++ b/api-reference-v2/api-reference/api-key/api-key--create.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/api_keys +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/api-key/api-key--retrieve.mdx b/api-reference-v2/api-reference/api-key/api-key--retrieve.mdx new file mode 100644 index 000000000000..13b87953f1b7 --- /dev/null +++ b/api-reference-v2/api-reference/api-key/api-key--retrieve.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/api_keys/{key_id} +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/api-key/api-key--revoke.mdx b/api-reference-v2/api-reference/api-key/api-key--revoke.mdx new file mode 100644 index 000000000000..37a9c9fcc092 --- /dev/null +++ b/api-reference-v2/api-reference/api-key/api-key--revoke.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /v2/api_keys/{key_id} +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/api-key/api-key--update.mdx b/api-reference-v2/api-reference/api-key/api-key--update.mdx new file mode 100644 index 000000000000..8d1b6e2ee115 --- /dev/null +++ b/api-reference-v2/api-reference/api-key/api-key--update.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /v2/api_keys/{key_id} +--- diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index 27bc39d07622..975eb2915072 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -50,14 +50,6 @@ "api-reference/merchant-account/merchant-account--update" ] }, - { - "group": "Merchant Connector Account", - "pages": [ - "api-reference/merchant-connector-account/merchant-connector--create", - "api-reference/merchant-connector-account/merchant-connector--retrieve", - "api-reference/merchant-connector-account/merchant-connector--update" - ] - }, { "group": "Business Profile", "pages": [ @@ -71,6 +63,23 @@ "api-reference/business-profile/business-profile--retrieve-default-fallback-routing-algorithm" ] }, + { + "group": "Merchant Connector Account", + "pages": [ + "api-reference/merchant-connector-account/merchant-connector--create", + "api-reference/merchant-connector-account/merchant-connector--retrieve", + "api-reference/merchant-connector-account/merchant-connector--update" + ] + }, + { + "group": "API Key", + "pages": [ + "api-reference/api-key/api-key--create", + "api-reference/api-key/api-key--retrieve", + "api-reference/api-key/api-key--update", + "api-reference/api-key/api-key--revoke" + ] + }, { "group": "Routing", "pages": [ diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 7b9cf0e48c87..6882e823905a 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1128,6 +1128,175 @@ } ] } + }, + "/v2/api_keys": { + "post": { + "tags": [ + "API Key" + ], + "summary": "API Key - Create", + "description": "API Key - Create\n\nCreate a new API Key for accessing our APIs from your servers. The plaintext API Key will be\ndisplayed only once on creation, so ensure you store it securely.", + "operationId": "Create an API Key", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "API Key created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateApiKeyResponse" + } + } + } + }, + "400": { + "description": "Invalid data" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/v2/api_keys/{key_id}": { + "get": { + "tags": [ + "API Key" + ], + "summary": "API Key - Retrieve", + "description": "API Key - Retrieve\n\nRetrieve information about the specified API Key.", + "operationId": "Retrieve an API Key", + "parameters": [ + { + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "API Key retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrieveApiKeyResponse" + } + } + } + }, + "404": { + "description": "API Key not found" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "put": { + "tags": [ + "API Key" + ], + "summary": "API Key - Update", + "description": "API Key - Update\n\nUpdate information for the specified API Key.", + "operationId": "Update an API Key", + "parameters": [ + { + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApiKeyRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "API Key updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrieveApiKeyResponse" + } + } + } + }, + "404": { + "description": "API Key not found" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "delete": { + "tags": [ + "API Key" + ], + "summary": "API Key - Revoke", + "description": "API Key - Revoke\n\nRevoke the specified API Key. Once revoked, the API Key can no longer be used for\nauthenticating with our APIs.", + "operationId": "Revoke an API Key", + "parameters": [ + { + "name": "key_id", + "in": "path", + "description": "The unique identifier for the API Key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "API Key revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RevokeApiKeyResponse" + } + } + } + }, + "404": { + "description": "API Key not found" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } } }, "components": { @@ -8436,8 +8605,7 @@ "required": [ "connector_type", "connector_name", - "profile_id", - "merchant_id" + "profile_id" ], "properties": { "connector_type": { @@ -8551,13 +8719,6 @@ ], "nullable": true }, - "merchant_id": { - "type": "string", - "description": "The identifier for the Merchant Account", - "example": "y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64, - "minLength": 1 - }, "additional_merchant_data": { "allOf": [ { diff --git a/api-reference/README.md b/api-reference/README.md new file mode 100644 index 000000000000..a883fc663dbf --- /dev/null +++ b/api-reference/README.md @@ -0,0 +1,45 @@ +# Api Reference + +We use the [openapi specification](https://swagger.io/specification) for the api reference. The openapi file is generated from the code base [openapi_spec.json](openapi_spec.json). + +## How to generate the file + +This file is automatically generated from our CI pipeline when the PR is raised. However if you want to generate it manually, the following command can be used + +```bash +cargo r -p openapi --features v1 +``` + +## Render the generated openapi spec file + +In order to render the openapi spec file, we use a tool called [mintlify](https://mintlify.com/). Local setup instructions can be found [here](https://mintlify.com/docs/development#development). Once the cli is installed, Run the following command to render the openapi spec + +- Navigate to the directory where `mint.json` exists + +```bash +cd api-reference +``` + +- Run the cli + +```bash +mintlify dev +``` + +## Add new routes to openapi + +If you have added new routes to the openapi. Then in order for them to be displayed on the mintlify, run the following commands + +- Switch to the directory where api reference ( mint.json ) exists + +```bash +cd api-reference +``` + +- Run the following command to generate the route files + +```bash +npx @mintlify/scraping@latest openapi-file openapi_spec.json -o api-reference +``` + +This will generate files in [api-reference](api-reference) folder. These routes should be added to the [mint.json](mint.json) file under navigation, under respective group. diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 9f0358462520..4a07d1179e41 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -4488,7 +4488,7 @@ ] } }, - "/api_keys/{merchant_id)": { + "/api_keys/{merchant_id}": { "post": { "tags": [ "API Key" @@ -4645,9 +4645,7 @@ "admin_api_key": [] } ] - } - }, - "/api_keys/{merchant_id)/{key_id}": { + }, "delete": { "tags": [ "API Key" diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 5b2432cda9ff..56924c541d35 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -767,10 +767,6 @@ pub struct MerchantConnectorCreate { // By default the ConnectorStatus is Active pub status: Option, - /// The identifier for the Merchant Account - #[schema(value_type = String, max_length = 64, min_length = 1, example = "y3oqhf46pyzuxjbcn2giaqnb44")] - pub merchant_id: id_type::MerchantId, - /// In case the merchant needs to store any additional sensitive data #[schema(value_type = Option)] pub additional_merchant_data: Option, diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 48b2e9d98c09..7a0bbb5cbe33 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -115,7 +115,7 @@ impl_api_event_type!( String, id_type::MerchantId, (id_type::MerchantId, String), - (&id_type::MerchantId, String), + (id_type::MerchantId, &String), (&id_type::MerchantId, &String), (&String, &String), (Option, Option, String), diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 2b02ec7f715e..691481858ffa 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -100,6 +100,12 @@ Never share your secret api keys. Keep them guarded and secure. // Routes for routing routes::routing::routing_create_config, routes::routing::routing_retrieve_config, + + // Routes for api keys + routes::api_keys::api_key_create, + routes::api_keys::api_key_retrieve, + routes::api_keys::api_key_update, + routes::api_keys::api_key_revoke, ), components(schemas( common_utils::types::MinorUnit, diff --git a/crates/openapi/src/routes/api_keys.rs b/crates/openapi/src/routes/api_keys.rs index 2956569bfb0e..7ed2afe91a15 100644 --- a/crates/openapi/src/routes/api_keys.rs +++ b/crates/openapi/src/routes/api_keys.rs @@ -1,10 +1,11 @@ +#[cfg(feature = "v1")] /// API Key - Create /// /// 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)", + path = "/api_keys/{merchant_id}", params(("merchant_id" = String, Path, description = "The unique identifier for the merchant account")), request_body= CreateApiKeyRequest, responses( @@ -17,6 +18,26 @@ )] pub async fn api_key_create() {} +#[cfg(feature = "v2")] +/// API Key - Create +/// +/// 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 = "/v2/api_keys", + 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" = [])) +)] +pub async fn api_key_create() {} + +#[cfg(feature = "v1")] /// API Key - Retrieve /// /// Retrieve information about the specified API Key. @@ -37,6 +58,27 @@ pub async fn api_key_create() {} )] pub async fn api_key_retrieve() {} +#[cfg(feature = "v2")] +/// API Key - Retrieve +/// +/// Retrieve information about the specified API Key. +#[utoipa::path( + get, + path = "/v2/api_keys/{key_id}", + params ( + ("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" = [])) +)] +pub async fn api_key_retrieve() {} + +#[cfg(feature = "v1")] /// API Key - Update /// /// Update information for the specified API Key. @@ -58,13 +100,35 @@ pub async fn api_key_retrieve() {} )] pub async fn api_key_update() {} +#[cfg(feature = "v2")] +/// API Key - Update +/// +/// Update information for the specified API Key. +#[utoipa::path( + put, + path = "/v2/api_keys/{key_id}", + request_body = UpdateApiKeyRequest, + params ( + ("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" = [])) +)] +pub async fn api_key_update() {} + +#[cfg(feature = "v1")] /// 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}", + 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") @@ -78,3 +142,24 @@ pub async fn api_key_update() {} security(("admin_api_key" = [])) )] pub async fn api_key_revoke() {} + +#[cfg(feature = "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 = "/v2/api_keys/{key_id}", + params ( + ("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" = [])) +)] +pub async fn api_key_revoke() {} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 59ed7ae605ce..4057c1a108ce 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -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 { 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 { 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 { 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 { 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 { 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 { 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?; diff --git a/crates/router/src/core/api_keys.rs b/crates/router/src/core/api_keys.rs index f06050c53036..6e907dbd91bf 100644 --- a/crates/router/src/core/api_keys.rs +++ b/crates/router/src/core/api_keys.rs @@ -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 { 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 { 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")? diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 7616e83beef1..8e679040c919 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -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, @@ -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, @@ -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, 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> { - 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 { - 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, @@ -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, @@ -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, @@ -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, @@ -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(), diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 3513984979b5..78bccad4a0d4 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -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, @@ -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, + req: HttpRequest, + json_payload: web::Json, +) -> 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, + req: HttpRequest, + path: web::Path, +) -> 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, @@ -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, @@ -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, + req: HttpRequest, + path: web::Path<(common_utils::id_type::MerchantId, String)>, + json_payload: web::Json, +) -> 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, @@ -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, + 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. diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 32bcc34d6bb5..8201380cc29f 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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}") diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 63c40580e534..60ecbe0a2bce 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -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 diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index f1de55282240..b1f12b10670a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -692,11 +692,11 @@ where } } -#[derive(Debug, Default)] -pub struct AdminApiAuthWithMerchantId(AdminApiAuth); +#[derive(Debug)] +pub struct AdminApiAuthWithMerchantIdFromRoute(pub id_type::MerchantId); #[async_trait] -impl AuthenticateAndFetch for AdminApiAuthWithMerchantId +impl AuthenticateAndFetch 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> { + 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 { + 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 AuthenticateAndFetch 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 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::(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 AuthenticateAndFetch 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::(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 AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute where