Skip to main content

qcs_api_client_common/configuration/
error.rs

1use std::{error::Error, path::PathBuf};
2
3use crate::configuration::{oidc::DISCOVERY_REQUIRED_SCOPE, tokens::PkceFlowError};
4
5use super::ClientConfigurationBuilderError;
6use super::secrets::SECRETS_READ_ONLY_VAR;
7
8/// Errors that can occur when loading a configuration.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum LoadError {
12    /// Failed to load config from a file.
13    #[error("Failed to load settings: {0}")]
14    Load(Box<dyn Error + Send + Sync + 'static>),
15    /// Failed to access or parse an environment variable.
16    #[error("Failed to load value from the environment variable {variable_name}: {message}")]
17    EnvVar {
18        /// The name of the environment variable.
19        variable_name: String,
20        /// The error message.
21        message: String,
22    },
23    /// Failed to load a file from a path.
24    #[error("Failed to load file from path {path:?}: {message}")]
25    Path {
26        /// The path that could not be loaded.
27        path: PathBuf,
28        /// The error message.
29        message: String,
30    },
31    /// The file could not be read or written to.
32    #[error(transparent)]
33    Io(#[from] std::io::Error),
34    /// Failed to use the builder to build a configuration.
35    #[error("Failed to build the ClientConfiguration: {0}")]
36    Build(#[from] ClientConfigurationBuilderError),
37    /// Provided profile not found.
38    #[error("Expected profile {0} in settings.profiles but it does not exist")]
39    ProfileNotFound(String),
40    /// Provided authorization server not found.
41    #[error("Expected auth server {0} in settings.auth_servers but it does not exist")]
42    AuthServerNotFound(String),
43    /// Failed to complete a PKCE login flow.
44    #[error("Failed to complete PKCE login: {0}")]
45    PkceFlow(#[from] PkceFlowError),
46    #[cfg(feature = "tracing-config")]
47    /// Failed to parse tracing filter. These should be a comma separated list of URL patterns. See
48    /// <https://wicg.github.io/urlpattern> for reference.
49    #[error("Could not parse tracing filter: {0}")]
50    TracingFilterParseError(#[from] crate::tracing_configuration::TracingFilterError),
51}
52
53impl<E: Error + 'static> From<shellexpand::LookupError<E>> for LoadError {
54    fn from(value: shellexpand::LookupError<E>) -> Self {
55        Self::EnvVar {
56            variable_name: value.var_name,
57            message: value.cause.to_string(),
58        }
59    }
60}
61
62impl From<figment::Error> for LoadError {
63    fn from(value: figment::Error) -> Self {
64        Self::Load(Box::new(value))
65    }
66}
67
68/// Errors that can occur when managing authorization tokens.
69#[derive(Debug, thiserror::Error)]
70pub enum TokenError {
71    /// No QCS API refresh token to use.
72    #[error("No refresh token is configured within the selected QCS credential.")]
73    NoRefreshToken,
74    /// No access token to use.
75    #[error("No access token has been requested.")]
76    NoAccessToken,
77    /// No access token to use.
78    #[error("Requested an access token for a configuration without credentials.")]
79    NoCredentials,
80    /// Access token is invalid.
81    #[error("The access token is invalid: {0}")]
82    InvalidAccessToken(jsonwebtoken::errors::Error),
83    /// No QCS API refresh token to use.
84    #[error("No auth server is configured within the selected QCS credential.")]
85    NoAuthServer,
86    /// Failure fetching a refreshed access token from the QCS API.
87    #[error("Error fetching new token from the QCS API: {0}")]
88    Fetch(#[from] qcs_dependencies_client::reqwest::Error),
89    /// Catch all for errors returned from an [`super::ExternallyManaged`] refresh function.
90    #[error("Failed to request an externally managed access token: {0}")]
91    ExternallyManaged(String),
92    /// Failure writing the new access token to the secrets file.
93    #[error(
94        "Failed to write the new access token to the secrets file. Setting `{SECRETS_READ_ONLY_VAR}=true` in the environment will skip persistence of newly acquired tokens. Error details: {error}"
95    )]
96    Write {
97        /// The underlying write error.
98        error: WriteError,
99        /// The successfully refreshed OAuth session that failed to persist. The token is valid and can be used despite the write failure.
100        ///
101        /// Boxed to reduce the size of the `TokenError` enum and avoid `clippy::result_large_err` warnings.
102        oauth_session: Box<super::OAuthSession>,
103    },
104    /// Failure fetching the OIDC discovery document.
105    #[error("Failed to fetch the OIDC discovery document: {0}")]
106    Discovery(#[from] DiscoveryError),
107}
108
109/// Errors that can occur when attempting to fetch and process an OIDC discovery document.
110#[derive(Debug, thiserror::Error)]
111pub enum DiscoveryError {
112    #[error("invalid issuer URL: {0}")]
113    Url(#[from] url::ParseError),
114    #[error("error fetching discovery document: {0}")]
115    Fetch(#[from] qcs_dependencies_client::reqwest::Error),
116    #[error("failed to parse discovery document: {0}")]
117    Json(#[from] serde_json::Error),
118    #[error("issuer URL ({issuer}) is invalid: {reason}")]
119    InvalidIssuer { issuer: String, reason: String },
120    #[error("discovery document is invalid: {reason}")]
121    InvalidDocument { reason: String },
122    #[error("discovery document issuer ({document}) does not match the queried issuer ({query})")]
123    IssuerMismatch { document: String, query: String },
124    #[error("discovery document `supported_scopes` does not include the required minimum scope \"{expected}\", received: {0:?}", expected = DISCOVERY_REQUIRED_SCOPE)]
125    InvalidScopes(Vec<String>),
126}
127
128/// Errors that can occur when trying to write or update a configuration file.
129#[derive(Debug, thiserror::Error)]
130pub enum WriteError {
131    /// There was an IO error while updating the secrets file.
132    #[error(transparent)]
133    IoWithPath(#[from] IoErrorWithPath),
134    /// The file's contents are not valid TOML
135    #[error("File could not be read as TOML: {0}")]
136    InvalidToml(#[from] toml_edit::TomlError),
137    /// TOML table could not be found.
138    #[error("The table `{0}` does not exist.")]
139    MissingTable(String),
140    /// There was an error with time formatting
141    #[error("Error formatting time: {0}.")]
142    TimeFormat(#[from] time::error::Format),
143    /// There was an error writing or persisting the temporary secrets file during access token refresh.
144    #[error("Error writing or persisting temporary secrets file during access token refresh: {0}")]
145    TempFile(#[from] async_tempfile::Error),
146}
147
148/// A fallible IO operation that can result in a [`IoErrorWithPath`]
149#[derive(Debug)]
150pub enum IoOperation {
151    Open,
152    Read,
153    Write,
154    Rename { dest: PathBuf },
155    GetMetadata,
156    SetPermissions,
157    Flush,
158}
159
160/// An error wrapping [`std::io::Error`] that includes the path and operation as additional context.
161#[derive(Debug, thiserror::Error)]
162#[error("Io error while error performing {operation:?} on {path}: {error}")]
163pub struct IoErrorWithPath {
164    #[source]
165    pub error: std::io::Error,
166    pub path: PathBuf,
167    pub operation: IoOperation,
168}