feat(router): implement API endpoints for managing API keys (#511)

This commit is contained in:
Sanchith Hegde
2023-02-10 14:20:57 +05:30
committed by GitHub
parent 903b452146
commit 1bdc8955c2
35 changed files with 1759 additions and 99 deletions

View File

@ -94,6 +94,9 @@ pub enum StripeErrorCode {
#[error(error_type = StripeErrorType::InvalidRequestError, code = "resource_missing", message = "No such mandate")]
MandateNotFound,
#[error(error_type = StripeErrorType::InvalidRequestError, code = "resource_missing", message = "No such API key")]
ApiKeyNotFound,
#[error(error_type = StripeErrorType::InvalidRequestError, code = "parameter_missing", message = "Return url is not available")]
ReturnUrlUnavailable,
@ -383,6 +386,7 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
Self::MerchantConnectorAccountNotFound
}
errors::ApiErrorResponse::MandateNotFound => Self::MandateNotFound,
errors::ApiErrorResponse::ApiKeyNotFound => Self::ApiKeyNotFound,
errors::ApiErrorResponse::MandateValidationFailed { reason } => {
Self::PaymentIntentMandateInvalid { message: reason }
}
@ -453,6 +457,7 @@ impl actix_web::ResponseError for StripeErrorCode {
| Self::MerchantAccountNotFound
| Self::MerchantConnectorAccountNotFound
| Self::MandateNotFound
| Self::ApiKeyNotFound
| Self::DuplicateMerchantAccount
| Self::DuplicateMerchantConnectorAccount
| Self::DuplicatePaymentMethod

View File

@ -16,9 +16,10 @@ pub const REQUEST_TIME_OUT: u64 = 30;
pub(crate) const NO_ERROR_MESSAGE: &str = "No error message";
pub(crate) const NO_ERROR_CODE: &str = "No error code";
// General purpose base64 engine
// General purpose base64 engines
pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose =
base64::engine::general_purpose::STANDARD;
pub(crate) const BASE64_ENGINE_URL_SAFE: base64::engine::GeneralPurpose =
base64::engine::general_purpose::URL_SAFE;
pub(crate) const API_KEY_LENGTH: usize = 64;

View File

@ -1,4 +1,5 @@
pub mod admin;
pub mod api_keys;
pub mod configs;
pub mod customers;
pub mod errors;

View File

@ -7,7 +7,6 @@ use crate::{
consts,
core::errors::{self, RouterResponse, RouterResult, StorageErrorExt},
db::StorageInterface,
env::{self, Env},
pii::Secret,
services::api as service_api,
types::{
@ -20,12 +19,11 @@ use crate::{
#[inline]
pub fn create_merchant_api_key() -> String {
let id = Uuid::new_v4().simple();
match env::which() {
Env::Development => format!("dev_{id}"),
Env::Production => format!("prd_{id}"),
Env::Sandbox => format!("snd_{id}"),
}
format!(
"{}_{}",
router_env::env::prefix_for_env(),
Uuid::new_v4().simple()
)
}
pub async fn create_merchant_account(

View File

@ -0,0 +1,219 @@
use common_utils::{date_time, errors::CustomResult, fp_utils};
use error_stack::{report, IntoReport, ResultExt};
use masking::{PeekInterface, Secret};
use router_env::{instrument, tracing};
use crate::{
consts,
core::errors::{self, RouterResponse, StorageErrorExt},
db::StorageInterface,
services::ApplicationResponse,
types::{api, storage, transformers::ForeignInto},
utils,
};
// Defining new types `PlaintextApiKey` and `HashedApiKey` in the hopes of reducing the possibility
// of plaintext API key being stored in the data store.
pub struct PlaintextApiKey(Secret<String>);
pub struct HashedApiKey(String);
impl PlaintextApiKey {
const HASH_KEY_LEN: usize = 32;
const PREFIX_LEN: usize = 8;
pub fn new(length: usize) -> Self {
let key = common_utils::crypto::generate_cryptographically_secure_random_string(length);
Self(key.into())
}
pub fn new_hash_key() -> [u8; Self::HASH_KEY_LEN] {
common_utils::crypto::generate_cryptographically_secure_random_bytes()
}
pub fn new_key_id() -> String {
let env = router_env::env::prefix_for_env();
utils::generate_id(consts::ID_LENGTH, env)
}
pub fn prefix(&self) -> String {
self.0.peek().chars().take(Self::PREFIX_LEN).collect()
}
pub fn peek(&self) -> &str {
self.0.peek()
}
pub fn keyed_hash(&self, key: &[u8; Self::HASH_KEY_LEN]) -> HashedApiKey {
/*
Decisions regarding API key hashing algorithm chosen:
- Since API key hash verification would be done for each request, there is a requirement
for the hashing to be quick.
- Password hashing algorithms would not be suitable for this purpose as they're designed to
prevent brute force attacks, considering that the same password could be shared across
multiple sites by the user.
- Moreover, password hash verification happens once per user session, so the delay involved
is negligible, considering the security benefits it provides.
While with API keys (assuming uniqueness of keys across the application), the delay
involved in hashing (with the use of a password hashing algorithm) becomes significant,
considering that it must be done per request.
- Since we are the only ones generating API keys and are able to guarantee their uniqueness,
a simple hash algorithm is sufficient for this purpose.
Hash algorithms considered:
- Password hashing algorithms: Argon2id and PBKDF2
- Simple hashing algorithms: HMAC-SHA256, HMAC-SHA512, BLAKE3
After benchmarking the simple hashing algorithms, we decided to go with the BLAKE3 keyed
hashing algorithm, with a randomly generated key for the hash key.
*/
HashedApiKey(
blake3::keyed_hash(key, self.0.peek().as_bytes())
.to_hex()
.to_string(),
)
}
pub fn verify_hash(
&self,
key: &[u8; Self::HASH_KEY_LEN],
stored_api_key: &HashedApiKey,
) -> CustomResult<(), errors::ApiKeyError> {
// Converting both hashes to `blake3::Hash` since it provides constant-time equality checks
let provided_api_key_hash = blake3::keyed_hash(key, self.0.peek().as_bytes());
let stored_api_key_hash = blake3::Hash::from_hex(&stored_api_key.0)
.into_report()
.change_context(errors::ApiKeyError::FailedToReadHashFromHex)?;
fp_utils::when(provided_api_key_hash != stored_api_key_hash, || {
Err(errors::ApiKeyError::HashVerificationFailed).into_report()
})
}
}
#[instrument(skip_all)]
pub async fn create_api_key(
store: &dyn StorageInterface,
api_key: api::CreateApiKeyRequest,
merchant_id: String,
) -> RouterResponse<api::CreateApiKeyResponse> {
let hash_key = PlaintextApiKey::new_hash_key();
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
let api_key = storage::ApiKeyNew {
key_id: PlaintextApiKey::new_key_id(),
merchant_id,
name: api_key.name,
description: api_key.description,
hash_key: Secret::from(hex::encode(hash_key)),
hashed_api_key: plaintext_api_key.keyed_hash(&hash_key).into(),
prefix: plaintext_api_key.prefix(),
created_at: date_time::now(),
expires_at: api_key.expiration.into(),
last_used: None,
};
let api_key = store
.insert_api_key(api_key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to insert new API key")?;
Ok(ApplicationResponse::Json(
(api_key, plaintext_api_key).foreign_into(),
))
}
#[instrument(skip_all)]
pub async fn retrieve_api_key(
store: &dyn StorageInterface,
key_id: &str,
) -> RouterResponse<api::RetrieveApiKeyResponse> {
let api_key = store
.find_api_key_optional(key_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed
.attach_printable("Failed to retrieve new API key")?
.ok_or(report!(errors::ApiErrorResponse::ApiKeyNotFound))?; // If retrieve returned `None`
Ok(ApplicationResponse::Json(api_key.foreign_into()))
}
#[instrument(skip_all)]
pub async fn update_api_key(
store: &dyn StorageInterface,
key_id: &str,
api_key: api::UpdateApiKeyRequest,
) -> RouterResponse<api::RetrieveApiKeyResponse> {
let api_key = store
.update_api_key(key_id.to_owned(), api_key.foreign_into())
.await
.map_err(|err| err.to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound))?;
Ok(ApplicationResponse::Json(api_key.foreign_into()))
}
#[instrument(skip_all)]
pub async fn revoke_api_key(
store: &dyn StorageInterface,
key_id: &str,
) -> RouterResponse<api::RevokeApiKeyResponse> {
let revoked = store
.revoke_api_key(key_id)
.await
.map_err(|err| err.to_not_found_response(errors::ApiErrorResponse::ApiKeyNotFound))?;
Ok(ApplicationResponse::Json(api::RevokeApiKeyResponse {
key_id: key_id.to_owned(),
revoked,
}))
}
#[instrument(skip_all)]
pub async fn list_api_keys(
store: &dyn StorageInterface,
merchant_id: String,
limit: Option<i64>,
offset: Option<i64>,
) -> RouterResponse<Vec<api::RetrieveApiKeyResponse>> {
let api_keys = store
.list_api_keys_by_merchant_id(&merchant_id, limit, offset)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to list merchant API keys")?;
let api_keys = api_keys
.into_iter()
.map(ForeignInto::foreign_into)
.collect();
Ok(ApplicationResponse::Json(api_keys))
}
impl From<HashedApiKey> for storage::HashedApiKey {
fn from(hashed_api_key: HashedApiKey) -> Self {
hashed_api_key.0.into()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_hashing_and_verification() {
let plaintext_api_key = PlaintextApiKey::new(consts::API_KEY_LENGTH);
let hash_key = PlaintextApiKey::new_hash_key();
let hashed_api_key = plaintext_api_key.keyed_hash(&hash_key);
assert_ne!(
plaintext_api_key.0.peek().as_bytes(),
hashed_api_key.0.as_bytes()
);
plaintext_api_key
.verify_hash(&hash_key, &hashed_api_key)
.unwrap();
}
}

View File

@ -390,3 +390,11 @@ pub enum WebhooksFlowError {
#[error("Webhook not received by merchant")]
NotReceivedByMerchant,
}
#[derive(Debug, thiserror::Error)]
pub enum ApiKeyError {
#[error("Failed to read API key hash from hexadecimal string")]
FailedToReadHashFromHex,
#[error("Failed to verify provided API key hash against stored API key hash")]
HashVerificationFailed,
}

View File

@ -133,6 +133,8 @@ pub enum ApiErrorResponse {
ResourceIdNotFound,
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Mandate does not exist in our records")]
MandateNotFound,
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "API Key does not exist in our records")]
ApiKeyNotFound,
#[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "Return URL is not configured and not passed in payments request")]
ReturnUrlUnavailable,
#[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "This refund is not possible through Hyperswitch. Please raise the refund through {connector} dashboard")]
@ -231,7 +233,8 @@ impl actix_web::ResponseError for ApiErrorResponse {
| Self::IncorrectConnectorNameGiven
| Self::ResourceIdNotFound
| Self::ConfigNotFound
| Self::AddressNotFound => StatusCode::BAD_REQUEST, // 400
| Self::AddressNotFound
| Self::ApiKeyNotFound => StatusCode::BAD_REQUEST, // 400
Self::DuplicateMerchantAccount
| Self::DuplicateMerchantConnectorAccount
| Self::DuplicatePaymentMethod

View File

@ -1,4 +1,5 @@
pub mod address;
pub mod api_keys;
pub mod cache;
pub mod configs;
pub mod connector_response;
@ -35,23 +36,24 @@ pub trait StorageInterface:
Send
+ Sync
+ dyn_clone::DynClone
+ payment_attempt::PaymentAttemptInterface
+ mandate::MandateInterface
+ address::AddressInterface
+ api_keys::ApiKeyInterface
+ configs::ConfigInterface
+ connector_response::ConnectorResponseInterface
+ customers::CustomerInterface
+ ephemeral_key::EphemeralKeyInterface
+ events::EventInterface
+ merchant_account::MerchantAccountInterface
+ merchant_connector_account::MerchantConnectorAccountInterface
+ merchant_connector_account::ConnectorAccessToken
+ locker_mock_up::LockerMockUpInterface
+ mandate::MandateInterface
+ merchant_account::MerchantAccountInterface
+ merchant_connector_account::ConnectorAccessToken
+ merchant_connector_account::MerchantConnectorAccountInterface
+ payment_attempt::PaymentAttemptInterface
+ payment_intent::PaymentIntentInterface
+ payment_method::PaymentMethodInterface
+ process_tracker::ProcessTrackerInterface
+ refund::RefundInterface
+ queue::QueueInterface
+ ephemeral_key::EphemeralKeyInterface
+ connector_response::ConnectorResponseInterface
+ refund::RefundInterface
+ reverse_lookup::ReverseLookupInterface
+ 'static
{

View File

@ -0,0 +1,138 @@
use error_stack::IntoReport;
use super::{MockDb, Store};
use crate::{
connection::pg_connection,
core::errors::{self, CustomResult},
types::storage,
};
#[async_trait::async_trait]
pub trait ApiKeyInterface {
async fn insert_api_key(
&self,
api_key: storage::ApiKeyNew,
) -> CustomResult<storage::ApiKey, errors::StorageError>;
async fn update_api_key(
&self,
key_id: String,
api_key: storage::ApiKeyUpdate,
) -> CustomResult<storage::ApiKey, errors::StorageError>;
async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError>;
async fn find_api_key_optional(
&self,
key_id: &str,
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError>;
async fn list_api_keys_by_merchant_id(
&self,
merchant_id: &str,
limit: Option<i64>,
offset: Option<i64>,
) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError>;
}
#[async_trait::async_trait]
impl ApiKeyInterface for Store {
async fn insert_api_key(
&self,
api_key: storage::ApiKeyNew,
) -> CustomResult<storage::ApiKey, errors::StorageError> {
let conn = pg_connection(&self.master_pool).await;
api_key
.insert(&conn)
.await
.map_err(Into::into)
.into_report()
}
async fn update_api_key(
&self,
key_id: String,
api_key: storage::ApiKeyUpdate,
) -> CustomResult<storage::ApiKey, errors::StorageError> {
let conn = pg_connection(&self.master_pool).await;
storage::ApiKey::update_by_key_id(&conn, key_id, api_key)
.await
.map_err(Into::into)
.into_report()
}
async fn revoke_api_key(&self, key_id: &str) -> CustomResult<bool, errors::StorageError> {
let conn = pg_connection(&self.master_pool).await;
storage::ApiKey::revoke_by_key_id(&conn, key_id)
.await
.map_err(Into::into)
.into_report()
}
async fn find_api_key_optional(
&self,
key_id: &str,
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
let conn = pg_connection(&self.master_pool).await;
storage::ApiKey::find_optional_by_key_id(&conn, key_id)
.await
.map_err(Into::into)
.into_report()
}
async fn list_api_keys_by_merchant_id(
&self,
merchant_id: &str,
limit: Option<i64>,
offset: Option<i64>,
) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError> {
let conn = pg_connection(&self.master_pool).await;
storage::ApiKey::find_by_merchant_id(&conn, merchant_id, limit, offset)
.await
.map_err(Into::into)
.into_report()
}
}
#[async_trait::async_trait]
impl ApiKeyInterface for MockDb {
async fn insert_api_key(
&self,
_api_key: storage::ApiKeyNew,
) -> CustomResult<storage::ApiKey, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}
async fn update_api_key(
&self,
_key_id: String,
_api_key: storage::ApiKeyUpdate,
) -> CustomResult<storage::ApiKey, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}
async fn revoke_api_key(&self, _key_id: &str) -> CustomResult<bool, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}
async fn find_api_key_optional(
&self,
_key_id: &str,
) -> CustomResult<Option<storage::ApiKey>, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}
async fn list_api_keys_by_merchant_id(
&self,
_merchant_id: &str,
_limit: Option<i64>,
_offset: Option<i64>,
) -> CustomResult<Vec<storage::ApiKey>, errors::StorageError> {
// [#172]: Implement function for `MockDb`
Err(errors::StorageError::MockDbError)?
}
}

View File

@ -49,6 +49,7 @@ pub mod headers {
pub const ACCEPT: &str = "Accept";
pub const X_API_VERSION: &str = "X-ApiVersion";
pub const DATE: &str = "Date";
pub const X_MERCHANT_ID: &str = "X-Merchant-Id";
}
pub mod pii {
@ -95,7 +96,9 @@ pub fn mk_app(
#[cfg(feature = "olap")]
{
server_app = server_app.service(routes::MerchantAccount::server(state.clone()));
server_app = server_app
.service(routes::MerchantAccount::server(state.clone()))
.service(routes::ApiKeys::server(state.clone()));
}
#[cfg(feature = "stripe")]

View File

@ -17,7 +17,10 @@ Our APIs accept and return JSON in the HTTP body, and return standard HTTP respo
You can consume the APIs directly using your favorite HTTP/REST library.
We have a testing environment referred to "sandbox", which you can setup to test API calls without
affecting production data. Currently, our sandbox environment is live while our production environment is under development and will be available soon. You can sign up on our Dashboard to get API keys to access Hyperswitch API.
affecting production data.
Currently, our sandbox environment is live while our production environment is under development
and will be available soon.
You can sign up on our Dashboard to get API keys to access Hyperswitch API.
### Environment
@ -32,13 +35,13 @@ Use the following base URLs when making requests to the APIs:
When you sign up on our [dashboard](https://app.hyperswitch.io) and create a merchant
account, you are given a secret key (also referred as api-key) and a publishable key.
You may authenticate all API requests with Hyperswitch server by providing the appropriate key in the
request Authorization header.
You may authenticate all API requests with Hyperswitch server by providing the appropriate key in
the request Authorization header.
| Key | Description |
|---------------|-----------------------------------------------------------------------------------------------|
| Sandbox | Private key. Used to authenticate all API requests from your merchant server |
| Production | Unique identifier for your account. Used to authenticate API requests from your apps client |
| Production | Unique identifier for your account. Used to authenticate API requests from your app's client |
Never share your secret api keys. Keep them guarded and secure.
"#,
@ -47,13 +50,14 @@ Never share your secret api keys. Keep them guarded and secure.
(url = "https://sandbox.hyperswitch.io", description = "Sandbox Environment")
),
tags(
(name = "Merchant Account"),// , description = "Create and manage merchant accounts"),
(name = "Merchant Connector Account"),// , description = "Create and manage merchant connector accounts"),
(name = "Payments"),// , description = "Create and manage one-time payments, recurring payments and mandates"),
(name = "Refunds"),// , description = "Create and manage refunds for successful payments"),
(name = "Mandates"),// , description = "Manage mandates"),
(name = "Customers"),// , description = "Create and manage customers"),
(name = "Payment Methods")// , description = "Create and manage payment methods of customers")
(name = "Merchant Account", description = "Create and manage merchant accounts"),
(name = "Merchant Connector Account", description = "Create and manage merchant connector accounts"),
(name = "Payments", description = "Create and manage one-time payments, recurring payments and mandates"),
(name = "Refunds", description = "Create and manage refunds for successful payments"),
(name = "Mandates", description = "Manage mandates"),
(name = "Customers", description = "Create and manage customers"),
(name = "Payment Methods", description = "Create and manage payment methods of customers"),
(name = "API Key", description = "Create and manage API Keys"),
),
paths(
crate::routes::refunds::refunds_create,
@ -92,6 +96,11 @@ Never share your secret api keys. Keep them guarded and secure.
crate::routes::customers::customers_retrieve,
crate::routes::customers::customers_update,
crate::routes::customers::customers_delete,
crate::routes::api_keys::api_key_create,
crate::routes::api_keys::api_key_retrieve,
crate::routes::api_keys::api_key_update,
crate::routes::api_keys::api_key_revoke,
crate::routes::api_keys::api_key_list,
),
components(schemas(
crate::types::api::refunds::RefundRequest,
@ -181,6 +190,12 @@ Never share your secret api keys. Keep them guarded and secure.
crate::types::api::admin::MerchantConnectorId,
crate::types::api::admin::MerchantDetails,
crate::types::api::admin::WebhookDetails,
crate::types::api::api_keys::ApiKeyExpiration,
crate::types::api::api_keys::CreateApiKeyRequest,
crate::types::api::api_keys::CreateApiKeyResponse,
crate::types::api::api_keys::RetrieveApiKeyResponse,
crate::types::api::api_keys::RevokeApiKeyResponse,
crate::types::api::api_keys::UpdateApiKeyRequest
))
)]
pub struct ApiDoc;

View File

@ -1,4 +1,5 @@
pub mod admin;
pub mod api_keys;
pub mod app;
pub mod configs;
pub mod customers;
@ -13,7 +14,7 @@ pub mod refunds;
pub mod webhooks;
pub use self::app::{
AppState, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
ApiKeys, AppState, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
};
#[cfg(feature = "stripe")]

View File

@ -0,0 +1,209 @@
use actix_web::{web, HttpRequest, Responder};
use error_stack::{IntoReport, ResultExt};
use router_env::{instrument, tracing, Flow};
use super::app::AppState;
use crate::{
core::{
api_keys,
errors::{self, RouterResult},
},
services::{api, authentication as auth},
types::api as api_types,
};
/// 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",
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"
)]
#[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 payload = json_payload.into_inner();
api::server_wrap(
state.get_ref(),
&req,
payload,
|state, _, payload| async {
let merchant_id = get_merchant_id_header(&req)?;
api_keys::create_api_key(&*state.store, payload, merchant_id).await
},
&auth::AdminApiAuth,
)
.await
}
/// API Key - Retrieve
///
/// Retrieve information about the specified API Key.
#[utoipa::path(
get,
path = "/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"
)]
#[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 key_id = path.into_inner();
api::server_wrap(
state.get_ref(),
&req,
&key_id,
|state, _, key_id| api_keys::retrieve_api_key(&*state.store, key_id),
&auth::AdminApiAuth,
)
.await
}
/// API Key - Update
///
/// Update information for the specified API Key.
#[utoipa::path(
post,
path = "/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"
)]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyUpdate))]
pub async fn api_key_update(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<String>,
json_payload: web::Json<api_types::UpdateApiKeyRequest>,
) -> impl Responder {
let key_id = path.into_inner();
let payload = json_payload.into_inner();
api::server_wrap(
state.get_ref(),
&req,
(&key_id, payload),
|state, _, (key_id, payload)| api_keys::update_api_key(&*state.store, key_id, payload),
&auth::AdminApiAuth,
)
.await
}
/// 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/{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"
)]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyRevoke))]
pub async fn api_key_revoke(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<String>,
) -> impl Responder {
let key_id = path.into_inner();
api::server_wrap(
state.get_ref(),
&req,
&key_id,
|state, _, key_id| api_keys::revoke_api_key(&*state.store, key_id),
&auth::AdminApiAuth,
)
.await
}
/// API Key - List
///
/// List all API Keys associated with your merchant account.
#[utoipa::path(
get,
path = "/api_keys/list",
params(
("limit" = Option<i64>, Query, description = "The maximum number of API Keys to include in the response"),
("skip" = Option<i64>, Query, description = "The number of API Keys to skip when retrieving the list of API keys."),
),
responses(
(status = 200, description = "List of API Keys retrieved successfully", body = Vec<RetrieveApiKeyResponse>),
),
tag = "API Key",
operation_id = "List all API Keys associated with a merchant account"
)]
#[instrument(skip_all, fields(flow = ?Flow::ApiKeyList))]
pub async fn api_key_list(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<api_types::ListApiKeyConstraints>,
) -> impl Responder {
let list_api_key_constraints = query.into_inner();
let limit = list_api_key_constraints.limit;
let offset = list_api_key_constraints.skip;
api::server_wrap(
state.get_ref(),
&req,
(&req, limit, offset),
|state, _, (req, limit, offset)| async move {
let merchant_id = get_merchant_id_header(req)?;
api_keys::list_api_keys(&*state.store, merchant_id, limit, offset).await
},
&auth::AdminApiAuth,
)
.await
}
fn get_merchant_id_header(req: &HttpRequest) -> RouterResult<String> {
use crate::headers::X_MERCHANT_ID;
req.headers()
.get(X_MERCHANT_ID)
.ok_or_else(|| errors::ApiErrorResponse::InvalidRequestData {
message: format!("Missing header: `{X_MERCHANT_ID}`"),
})
.into_report()?
.to_str()
.into_report()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: X_MERCHANT_ID,
})
.attach_printable(
"Failed to convert header value to string, \
possibly contains non-printable or non-ASCII characters",
)
.map(|s| s.to_owned())
}

View File

@ -1,8 +1,8 @@
use actix_web::{web, Scope};
#[cfg(feature = "olap")]
use super::admin::*;
use super::health::*;
#[cfg(feature = "olap")]
use super::{admin::*, api_keys::*};
#[cfg(any(feature = "olap", feature = "oltp"))]
use super::{configs::*, customers::*, mandates::*, payments::*, payouts::*, refunds::*};
#[cfg(feature = "oltp")]
@ -334,3 +334,21 @@ impl Configs {
)
}
}
pub struct ApiKeys;
#[cfg(feature = "olap")]
impl ApiKeys {
pub fn server(state: AppState) -> Scope {
web::scope("/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::post().to(api_key_update))
.route(web::delete().to(api_key_revoke)),
)
}
}

View File

@ -1,4 +1,5 @@
pub mod admin;
pub mod api_keys;
pub mod configs;
pub mod customers;
pub mod enums;
@ -13,7 +14,8 @@ use std::{fmt::Debug, str::FromStr};
use error_stack::{report, IntoReport, ResultExt};
pub use self::{
admin::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*, webhooks::*,
admin::*, api_keys::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*,
webhooks::*,
};
use super::ErrorResponse;
use crate::{

View File

@ -0,0 +1,4 @@
pub use api_models::api_keys::{
ApiKeyExpiration, CreateApiKeyRequest, CreateApiKeyResponse, ListApiKeyConstraints,
RetrieveApiKeyResponse, RevokeApiKeyResponse, UpdateApiKeyRequest,
};

View File

@ -1,4 +1,5 @@
pub mod address;
pub mod api_keys;
pub mod configs;
pub mod connector_response;
pub mod customers;
@ -22,7 +23,8 @@ pub mod refund;
pub mod kv;
pub use self::{
address::*, configs::*, connector_response::*, customers::*, events::*, locker_mock_up::*,
mandate::*, merchant_account::*, merchant_connector_account::*, payment_attempt::*,
payment_intent::*, payment_method::*, process_tracker::*, refund::*, reverse_lookup::*,
address::*, api_keys::*, configs::*, connector_response::*, customers::*, events::*,
locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
payment_attempt::*, payment_intent::*, payment_method::*, process_tracker::*, refund::*,
reverse_lookup::*,
};

View File

@ -0,0 +1 @@
pub use storage_models::api_keys::{ApiKey, ApiKeyNew, ApiKeyUpdate, HashedApiKey};

View File

@ -398,3 +398,68 @@ impl From<F<api_models::payments::AddressDetails>> for F<storage_models::address
.into()
}
}
impl
From<
F<(
storage_models::api_keys::ApiKey,
crate::core::api_keys::PlaintextApiKey,
)>,
> for F<api_models::api_keys::CreateApiKeyResponse>
{
fn from(
item: F<(
storage_models::api_keys::ApiKey,
crate::core::api_keys::PlaintextApiKey,
)>,
) -> Self {
use masking::StrongSecret;
let (api_key, plaintext_api_key) = item.0;
api_models::api_keys::CreateApiKeyResponse {
key_id: api_key.key_id.clone(),
merchant_id: api_key.merchant_id,
name: api_key.name,
description: api_key.description,
api_key: StrongSecret::from(format!(
"{}-{}",
api_key.key_id,
plaintext_api_key.peek().to_owned()
)),
created: api_key.created_at,
expiration: api_key.expires_at.into(),
}
.into()
}
}
impl From<F<storage_models::api_keys::ApiKey>> for F<api_models::api_keys::RetrieveApiKeyResponse> {
fn from(item: F<storage_models::api_keys::ApiKey>) -> Self {
let api_key = item.0;
api_models::api_keys::RetrieveApiKeyResponse {
key_id: api_key.key_id.clone(),
merchant_id: api_key.merchant_id,
name: api_key.name,
description: api_key.description,
prefix: format!("{}-{}", api_key.key_id, api_key.prefix).into(),
created: api_key.created_at,
expiration: api_key.expires_at.into(),
}
.into()
}
}
impl From<F<api_models::api_keys::UpdateApiKeyRequest>>
for F<storage_models::api_keys::ApiKeyUpdate>
{
fn from(item: F<api_models::api_keys::UpdateApiKeyRequest>) -> Self {
let api_key = item.0;
storage_models::api_keys::ApiKeyUpdate::Update {
name: api_key.name,
description: api_key.description,
expires_at: api_key.expiration.map(Into::into),
last_used: None,
}
.into()
}
}