use std::collections::{BTreeMap, HashSet};
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use openauth_core::context::AuthContext;
use openauth_core::error::OpenAuthError;
use openauth_core::options::SecondaryStorage;
use openauth_core::plugin::PluginRequest;
use serde::{Deserialize, Serialize};
pub type ApiKeyPermissions = BTreeMap<String, Vec<String>>;
pub type ApiKeyGeneratorFuture =
Pin<Box<dyn Future<Output = Result<String, OpenAuthError>> + Send + 'static>>;
pub type ApiKeyGenerator = Arc<dyn Fn(ApiKeyGeneratorInput) -> ApiKeyGeneratorFuture + Send + Sync>;
pub type ApiKeyGetterFuture<'a> =
Pin<Box<dyn Future<Output = Result<Option<String>, OpenAuthError>> + Send + 'a>>;
pub type ApiKeyGetter =
Arc<dyn for<'a> Fn(&'a AuthContext, &'a PluginRequest) -> ApiKeyGetterFuture<'a> + Send + Sync>;
pub type ApiKeyValidatorFuture<'a> =
Pin<Box<dyn Future<Output = Result<bool, OpenAuthError>> + Send + 'a>>;
pub type ApiKeyValidator =
Arc<dyn for<'a> Fn(&'a AuthContext, &'a str) -> ApiKeyValidatorFuture<'a> + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiKeyGeneratorInput {
pub length: usize,
pub prefix: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ApiKeyStorageMode {
Database,
SecondaryStorage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ApiKeyReference {
User,
Organization,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeyRateLimitOptions {
pub enabled: bool,
pub time_window: i64,
pub max_requests: i64,
}
impl Default for ApiKeyRateLimitOptions {
fn default() -> Self {
Self {
enabled: true,
time_window: 1000 * 60 * 60 * 24,
max_requests: 10,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiKeyExpirationOptions {
pub default_expires_in: Option<i64>,
pub disable_custom_expires_time: bool,
pub min_expires_in_days: i64,
pub max_expires_in_days: i64,
}
impl Default for ApiKeyExpirationOptions {
fn default() -> Self {
Self {
default_expires_in: None,
disable_custom_expires_time: false,
min_expires_in_days: 1,
max_expires_in_days: 365,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartingCharactersConfig {
pub should_store: bool,
pub characters_length: usize,
}
impl Default for StartingCharactersConfig {
fn default() -> Self {
Self {
should_store: true,
characters_length: 6,
}
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ApiKeyConfiguration {
pub config_id: Option<String>,
pub api_key_headers: Vec<String>,
pub disable_key_hashing: bool,
pub default_key_length: usize,
pub default_prefix: Option<String>,
pub maximum_prefix_length: usize,
pub minimum_prefix_length: usize,
pub require_name: bool,
pub maximum_name_length: usize,
pub minimum_name_length: usize,
pub enable_metadata: bool,
pub key_expiration: ApiKeyExpirationOptions,
pub rate_limit: ApiKeyRateLimitOptions,
pub enable_session_for_api_keys: bool,
pub default_permissions: Option<ApiKeyPermissions>,
#[serde(skip)]
pub custom_key_generator: Option<ApiKeyGenerator>,
#[serde(skip)]
pub custom_api_key_getter: Option<ApiKeyGetter>,
#[serde(skip)]
pub custom_api_key_validator: Option<ApiKeyValidator>,
pub storage: ApiKeyStorageMode,
pub fallback_to_database: bool,
#[serde(skip)]
pub custom_storage: Option<Arc<dyn SecondaryStorage>>,
pub defer_updates: bool,
pub reference: ApiKeyReference,
pub starting_characters: StartingCharactersConfig,
}
impl Default for ApiKeyConfiguration {
fn default() -> Self {
Self {
config_id: None,
api_key_headers: vec!["x-api-key".to_owned()],
disable_key_hashing: false,
default_key_length: 64,
default_prefix: None,
maximum_prefix_length: 32,
minimum_prefix_length: 1,
require_name: false,
maximum_name_length: 32,
minimum_name_length: 1,
enable_metadata: false,
key_expiration: ApiKeyExpirationOptions::default(),
rate_limit: ApiKeyRateLimitOptions::default(),
enable_session_for_api_keys: false,
default_permissions: None,
custom_key_generator: None,
custom_api_key_getter: None,
custom_api_key_validator: None,
storage: ApiKeyStorageMode::Database,
fallback_to_database: false,
custom_storage: None,
defer_updates: false,
reference: ApiKeyReference::User,
starting_characters: StartingCharactersConfig::default(),
}
}
}
impl fmt::Debug for ApiKeyConfiguration {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ApiKeyConfiguration")
.field("config_id", &self.config_id)
.field("api_key_headers", &self.api_key_headers)
.field("disable_key_hashing", &self.disable_key_hashing)
.field("default_key_length", &self.default_key_length)
.field("default_prefix", &self.default_prefix)
.field("maximum_prefix_length", &self.maximum_prefix_length)
.field("minimum_prefix_length", &self.minimum_prefix_length)
.field("require_name", &self.require_name)
.field("maximum_name_length", &self.maximum_name_length)
.field("minimum_name_length", &self.minimum_name_length)
.field("enable_metadata", &self.enable_metadata)
.field("key_expiration", &self.key_expiration)
.field("rate_limit", &self.rate_limit)
.field(
"enable_session_for_api_keys",
&self.enable_session_for_api_keys,
)
.field("default_permissions", &self.default_permissions)
.field(
"custom_key_generator",
&self
.custom_key_generator
.as_ref()
.map(|_| "<custom-key-generator>"),
)
.field(
"custom_api_key_getter",
&self
.custom_api_key_getter
.as_ref()
.map(|_| "<custom-api-key-getter>"),
)
.field(
"custom_api_key_validator",
&self
.custom_api_key_validator
.as_ref()
.map(|_| "<custom-api-key-validator>"),
)
.field("storage", &self.storage)
.field("fallback_to_database", &self.fallback_to_database)
.field(
"custom_storage",
&self.custom_storage.as_ref().map(|_| "<custom-storage>"),
)
.field("defer_updates", &self.defer_updates)
.field("reference", &self.reference)
.field("starting_characters", &self.starting_characters)
.finish()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ApiKeyOptions {
pub configuration: ApiKeyConfiguration,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ApiKeyOptionsError {
#[error("config_id is required for each API key configuration in the api-key plugin")]
MissingConfigId,
#[error("config_id must be unique for each API key configuration in the api-key plugin")]
DuplicateConfigId,
}
impl From<ApiKeyOptionsError> for OpenAuthError {
fn from(error: ApiKeyOptionsError) -> Self {
Self::InvalidConfig(error.to_string())
}
}
#[derive(Debug, Clone)]
pub(crate) struct ResolvedConfigurations {
configurations: Vec<ApiKeyConfiguration>,
}
impl ResolvedConfigurations {
pub fn single(configuration: ApiKeyConfiguration) -> Self {
Self {
configurations: vec![configuration],
}
}
pub fn multiple(configurations: Vec<ApiKeyConfiguration>) -> Result<Self, ApiKeyOptionsError> {
let mut seen = HashSet::new();
for configuration in &configurations {
let Some(config_id) = configuration.config_id.as_deref() else {
return Err(ApiKeyOptionsError::MissingConfigId);
};
if !seen.insert(config_id.to_owned()) {
return Err(ApiKeyOptionsError::DuplicateConfigId);
}
}
Ok(Self { configurations })
}
pub fn all(&self) -> &[ApiKeyConfiguration] {
&self.configurations
}
pub fn resolve(&self, config_id: Option<&str>) -> Result<ApiKeyConfiguration, OpenAuthError> {
if let Some(config_id) = config_id {
if let Some(configuration) = self
.configurations
.iter()
.find(|configuration| configuration.config_id.as_deref() == Some(config_id))
{
return Ok(with_default_config_id(configuration.clone()));
}
}
self.configurations
.iter()
.find(|configuration| {
configuration.config_id.is_none()
|| configuration.config_id.as_deref() == Some("default")
})
.cloned()
.map(with_default_config_id)
.ok_or_else(|| {
OpenAuthError::Api(
crate::api_key::errors::message(
crate::api_key::errors::NO_DEFAULT_API_KEY_CONFIGURATION_FOUND,
)
.to_owned(),
)
})
}
}
fn with_default_config_id(mut configuration: ApiKeyConfiguration) -> ApiKeyConfiguration {
if configuration.config_id.is_none() {
configuration.config_id = Some("default".to_owned());
}
configuration
}