qcs_api_client_common/configuration/
settings.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use figment::providers::Format;
5use figment::{providers::Toml, Figment};
6use serde::{Deserialize, Serialize};
7
8use super::{
9    env_or_default_quilc_url, env_or_default_qvm_url, expand_path_from_env_or_default, LoadError,
10    DEFAULT_API_URL, DEFAULT_GRPC_API_URL, DEFAULT_PROFILE_NAME, DEFAULT_QUILC_URL,
11    DEFAULT_QVM_URL,
12};
13
14/// Setting the `QCS_SETTINGS_FILE_PATH` environment variable will change which file is used for loading [`Settings`].
15pub const SETTINGS_PATH_VAR: &str = "QCS_SETTINGS_FILE_PATH";
16/// The default path that [`Settings`] will be loaded from;
17pub const DEFAULT_SETTINGS_PATH: &str = "~/.qcs/settings.toml";
18
19#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
20pub(crate) struct Settings {
21    #[serde(default = "default_profile_name")]
22    pub(crate) default_profile_name: String,
23    #[serde(default = "default_profiles")]
24    pub(crate) profiles: HashMap<String, Profile>,
25    #[serde(default = "default_auth_servers")]
26    pub(crate) auth_servers: HashMap<String, AuthServer>,
27    #[serde(skip)]
28    pub(crate) file_path: Option<PathBuf>,
29}
30
31impl Settings {
32    pub(crate) fn load() -> Result<Self, LoadError> {
33        let path = expand_path_from_env_or_default(SETTINGS_PATH_VAR, DEFAULT_SETTINGS_PATH)?;
34        #[cfg(feature = "tracing")]
35        tracing::debug!("loading QCS settings from {path:?}");
36        let mut settings: Self = Figment::from(Toml::file(&path)).extract()?;
37        settings.file_path = Some(path);
38        Ok(settings)
39    }
40}
41
42impl Default for Settings {
43    fn default() -> Self {
44        Self {
45            default_profile_name: default_profile_name(),
46            profiles: default_profiles(),
47            auth_servers: default_auth_servers(),
48            file_path: None,
49        }
50    }
51}
52
53fn default_profile_name() -> String {
54    DEFAULT_PROFILE_NAME.to_string()
55}
56
57fn default_profiles() -> HashMap<String, Profile> {
58    HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), Profile::default())])
59}
60
61fn default_auth_servers() -> HashMap<String, AuthServer> {
62    HashMap::from([(DEFAULT_PROFILE_NAME.to_string(), AuthServer::default())])
63}
64
65#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
66pub(crate) struct Profile {
67    /// URL of the QCS REST API.
68    #[serde(default = "default_api_url")]
69    pub(crate) api_url: String,
70    /// URL of the QCS gRPC API.
71    #[serde(default = "default_grpc_api_url")]
72    pub(crate) grpc_api_url: String,
73    /// Name of the auth server to use.
74    #[serde(default = "default_profile_name")]
75    pub(crate) auth_server_name: String,
76    /// Name of the credentials to use.
77    #[serde(default = "default_profile_name")]
78    pub(crate) credentials_name: String,
79    /// Application specific settings.
80    #[serde(default)]
81    pub(crate) applications: Applications,
82}
83
84impl Default for Profile {
85    fn default() -> Self {
86        Self {
87            api_url: DEFAULT_API_URL.to_string(),
88            grpc_api_url: DEFAULT_GRPC_API_URL.to_string(),
89            auth_server_name: DEFAULT_PROFILE_NAME.to_string(),
90            credentials_name: DEFAULT_PROFILE_NAME.to_string(),
91            applications: Applications::default(),
92        }
93    }
94}
95
96fn default_api_url() -> String {
97    DEFAULT_API_URL.to_string()
98}
99
100fn default_grpc_api_url() -> String {
101    DEFAULT_GRPC_API_URL.to_string()
102}
103
104pub(crate) const QCS_DEFAULT_CLIENT_ID_PRODUCTION: &str = "0oa3ykoirzDKpkfzk357";
105pub(crate) const QCS_DEFAULT_AUTH_ISSUER_PRODUCTION: &str =
106    "https://auth.qcs.rigetti.com/oauth2/aus8jcovzG0gW2TUG355";
107
108/// OAuth 2.0 authorization server.
109#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
110#[cfg_attr(feature = "python", pyo3::pyclass)]
111pub struct AuthServer {
112    /// OAuth 2.0 client id.
113    client_id: String,
114    /// OAuth 2.0 issuer URL.
115    ///
116    /// This is the base URL of the identity provider.
117    /// For Okta, this usually looks like `https://example.okta.com/oauth2/default`.
118    /// For Cognito, it might look like `https://cognito-idp.us-west-2.amazonaws.com/us-west-2_example`.
119    ///
120    /// Note that this is technically distinct from the `issuer` field in [`OidcDiscovery`],
121    /// which is the canonical URI that the identity provider uses to sign and validate tokens,
122    /// but the OpenID specification requires that they match exactly,
123    /// and that they match the `iss` claim in Tokens issued by this identity provider.
124    issuer: String,
125}
126
127impl Default for AuthServer {
128    fn default() -> Self {
129        Self {
130            client_id: QCS_DEFAULT_CLIENT_ID_PRODUCTION.to_string(),
131            issuer: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
132        }
133    }
134}
135
136impl AuthServer {
137    /// Create a new [`AuthServer`] with a ``client_id`` and ``issuer``.
138    #[must_use]
139    pub const fn new(client_id: String, issuer: String) -> Self {
140        Self { client_id, issuer }
141    }
142
143    /// Get the configured OAuth 2.0 client id.
144    #[must_use]
145    pub fn client_id(&self) -> &str {
146        &self.client_id
147    }
148
149    /// Set an OAuth 2.0 client id.
150    pub fn set_client_id(&mut self, id: String) {
151        self.client_id = id;
152    }
153
154    /// Get the OAuth 2.0 issuer URL.
155    #[must_use]
156    pub fn issuer(&self) -> &str {
157        &self.issuer
158    }
159
160    /// Set an OAuth 2.0 issuer URL.
161    pub fn set_issuer(&mut self, issuer: String) {
162        self.issuer = issuer;
163    }
164}
165
166#[derive(Deserialize, Clone, Debug, Default, PartialEq, Serialize)]
167pub(crate) struct Applications {
168    #[serde(default)]
169    pub(crate) pyquil: Pyquil,
170}
171
172#[derive(Deserialize, Clone, Debug, PartialEq, Serialize)]
173pub(crate) struct Pyquil {
174    #[serde(default = "env_or_default_quilc_url")]
175    pub(crate) quilc_url: String,
176
177    #[serde(default = "env_or_default_qvm_url")]
178    pub(crate) qvm_url: String,
179}
180
181impl Default for Pyquil {
182    fn default() -> Self {
183        Self {
184            quilc_url: DEFAULT_QUILC_URL.to_string(),
185            qvm_url: DEFAULT_QVM_URL.to_string(),
186        }
187    }
188}
189
190#[cfg(test)]
191mod test {
192    use std::path::PathBuf;
193
194    use super::{Settings, SETTINGS_PATH_VAR};
195
196    #[test]
197    fn returns_err_if_invalid_path_env() {
198        figment::Jail::expect_with(|jail| {
199            jail.set_env(SETTINGS_PATH_VAR, "/blah/doesnt_exist.toml");
200            Settings::load().expect_err("Should return error when a file cannot be found.");
201            Ok(())
202        });
203    }
204
205    #[test]
206    fn test_uses_defaults_incomplete_settings() {
207        figment::Jail::expect_with(|jail| {
208            let _ = jail.create_file("settings.toml", r#"default_profile_name = "TEST""#)?;
209            jail.set_env(SETTINGS_PATH_VAR, "settings.toml");
210            let loaded = Settings::load().expect("should load settings");
211            let expected = Settings {
212                default_profile_name: "TEST".to_string(),
213                file_path: Some(PathBuf::from("settings.toml")),
214                ..Settings::default()
215            };
216
217            assert_eq!(loaded, expected);
218
219            Ok(())
220        });
221    }
222
223    #[test]
224    fn loads_from_env_var_path() {
225        figment::Jail::expect_with(|jail| {
226            let settings = Settings {
227                default_profile_name: "TEST".to_string(),
228                file_path: Some(PathBuf::from("secrets.toml")),
229                ..Settings::default()
230            };
231            let settings_string =
232                toml::to_string(&settings).expect("Should be able to serialize settings");
233
234            _ = jail.create_file("secrets.toml", &settings_string)?;
235            jail.set_env(SETTINGS_PATH_VAR, "secrets.toml");
236
237            assert_eq!(settings, Settings::load().unwrap());
238
239            Ok(())
240        });
241    }
242}