Skip to main content

qcs_api_client_common/configuration/
error.rs

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