#![doc(html_favicon_url = "https://cipherstash.com/favicon.ico")]
#![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::expect_used)]
#![warn(clippy::panic)]
#![warn(clippy::mem_forget)]
#![warn(clippy::print_stdout)]
#![warn(clippy::print_stderr)]
#![warn(clippy::dbg_macro)]
#![warn(unreachable_pub)]
#![warn(unused_results)]
#![warn(clippy::todo)]
#![warn(clippy::unimplemented)]
#![cfg_attr(test, allow(clippy::unwrap_used))]
#![cfg_attr(test, allow(clippy::expect_used))]
#![cfg_attr(test, allow(clippy::panic))]
#![cfg_attr(test, allow(unused_results))]
use std::convert::Infallible;
use std::future::Future;
#[cfg(not(any(test, feature = "test-utils")))]
use std::time::Duration;
use vitaminc::protected::OpaqueDebug;
use zeroize::ZeroizeOnDrop;
mod access_key;
mod access_key_refresher;
mod access_key_strategy;
mod auto_refresh;
mod auto_strategy;
mod device_client;
mod device_code;
mod oauth_refresher;
mod oauth_strategy;
mod refresher;
mod service_token;
mod token;
#[cfg(any(test, feature = "test-utils"))]
mod static_token_strategy;
pub use access_key::{AccessKey, InvalidAccessKey};
pub use access_key_strategy::{AccessKeyStrategy, AccessKeyStrategyBuilder};
pub use auto_strategy::{AutoStrategy, AutoStrategyBuilder};
pub use device_code::{DeviceCodeStrategy, DeviceCodeStrategyBuilder, PendingDeviceCode};
pub use oauth_strategy::{OAuthStrategy, OAuthStrategyBuilder};
pub use service_token::ServiceToken;
#[cfg(any(test, feature = "test-utils"))]
pub use static_token_strategy::StaticTokenStrategy;
pub use token::Token;
pub use device_client::{bind_client_device, DeviceClientError};
pub use stack_profile::DeviceIdentity;
#[cfg_attr(doc, aquamarine::aquamarine)]
pub trait AuthStrategy: Send {
fn get_token(self) -> impl Future<Output = Result<ServiceToken, AuthError>> + Send;
}
#[derive(Clone, OpaqueDebug, ZeroizeOnDrop, serde::Deserialize, serde::Serialize)]
#[serde(transparent)]
pub struct SecretToken(String);
impl SecretToken {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
#[non_exhaustive]
pub enum AuthError {
#[error("HTTP request failed: {0}")]
Request(#[from] reqwest::Error),
#[error("Authorization was denied")]
AccessDenied,
#[error("Invalid grant")]
InvalidGrant,
#[error("Invalid client")]
InvalidClient,
#[error("Invalid URL: {0}")]
InvalidUrl(#[from] url::ParseError),
#[error("Unsupported region: {0}")]
Region(#[from] cts_common::RegionError),
#[error("Invalid workspace CRN: {0}")]
InvalidCrn(cts_common::InvalidCrn),
#[error("Workspace CRN is required when using an access key — set CS_WORKSPACE_CRN or call AutoStrategyBuilder::with_workspace_crn")]
MissingWorkspaceCrn,
#[error("Not authenticated")]
NotAuthenticated,
#[error("Token expired")]
TokenExpired,
#[error("Invalid access key: {0}")]
InvalidAccessKey(#[from] access_key::InvalidAccessKey),
#[error("Invalid token: {0}")]
InvalidToken(String),
#[error("Server error: {0}")]
Server(String),
#[error("Token store error: {0}")]
Store(#[from] stack_profile::ProfileError),
}
impl From<Infallible> for AuthError {
fn from(never: Infallible) -> Self {
match never {}
}
}
pub(crate) fn cts_base_url_from_env() -> Result<Option<url::Url>, AuthError> {
match std::env::var("CS_CTS_HOST") {
Ok(val) if !val.is_empty() => Ok(Some(val.parse()?)),
_ => Ok(None),
}
}
pub(crate) fn ensure_trailing_slash(mut url: url::Url) -> url::Url {
if !url.path().ends_with('/') {
url.set_path(&format!("{}/", url.path()));
}
url
}
pub(crate) fn http_client() -> reqwest::Client {
#[cfg(any(test, feature = "test-utils"))]
{
reqwest::Client::builder()
.pool_max_idle_per_host(10)
.build()
.unwrap_or_else(|_| reqwest::Client::new())
}
#[cfg(not(any(test, feature = "test-utils")))]
{
reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.pool_idle_timeout(Duration::from_secs(5))
.pool_max_idle_per_host(10)
.build()
.unwrap_or_else(|_| reqwest::Client::new())
}
}