oracle_nosql_rust_sdk/handle_builder.rs
1//
2// Copyright (c) 2024, 2025 Oracle and/or its affiliates. All rights reserved.
3//
4// Licensed under the Universal Permissive License v 1.0 as shown at
5// https://oss.oracle.com/licenses/upl/
6//
7//! Builder for creating a [`NoSQL Handle`](crate::Handle)
8//!
9
10use base64::prelude::{Engine as _, BASE64_STANDARD};
11use std::default::Default;
12use std::env;
13use std::result::Result;
14use std::sync::Arc;
15use std::time::{Duration, SystemTime, UNIX_EPOCH};
16
17use crate::auth_common::authentication_provider::AuthenticationProvider;
18use crate::auth_common::config_file_authentication_provider::ConfigFileAuthenticationProvider;
19use crate::auth_common::instance_principal_auth_provider::InstancePrincipalAuthProvider;
20use crate::error::{ia_err, NoSQLError};
21use crate::handle::Handle;
22use reqwest::header::HeaderValue;
23use reqwest::Client;
24use reqwest::{header::HeaderMap, Certificate};
25use serde_derive::Deserialize;
26
27use crate::region::{file_to_string, string_to_region, Region};
28
29/// Builder used to set all the parameters to create a [`NoSQL Handle`](crate::Handle).
30///
31/// See [Configuring the SDK](index.html#configuring-the-sdk) for a detailed description of creating configurations for
32/// various Oracle NoSQL Database instance types (cloud, on-premises, etc.).
33///
34#[derive(Default, Debug, Clone)]
35pub struct HandleBuilder {
36 pub(crate) endpoint: String,
37 pub(crate) timeout: Option<Duration>,
38 pub(crate) region: Option<Region>,
39 // TODO
40 //pub(crate) allow_imds: bool,
41 pub(crate) use_https: bool,
42 pub(crate) mode: HandleMode,
43 pub(crate) add_cert: Option<Certificate>,
44 pub(crate) client: Option<Client>,
45 pub(crate) accept_invalid_certs: bool,
46 pub(crate) auth_type: AuthType,
47 // auth uses a tokio Mutex because we occasionally hold a lock across awaits
48 pub(crate) auth: Arc<tokio::sync::Mutex<AuthConfig>>,
49 // For doc testing
50 pub(crate) in_test: bool,
51 // For error messaging
52 pub(crate) from_environment: bool,
53 pub(crate) default_compartment_id: String,
54}
55
56#[derive(Default, Debug)]
57pub(crate) struct AuthConfig {
58 pub(crate) provider: AuthProvider,
59}
60
61#[derive(Default, Debug)]
62pub(crate) enum AuthProvider {
63 File {
64 //path: String,
65 //profile: String,
66 provider: Box<dyn AuthenticationProvider>,
67 },
68 Instance {
69 provider: Box<dyn AuthenticationProvider>,
70 // TODO: last_refresh
71 // TODO: tenantId, compartmentId, region, domain, etc...
72 },
73 Resource {
74 provider: Box<dyn AuthenticationProvider>,
75 // TODO: is refreshable? expiration, etc
76 },
77 External {
78 provider: Box<dyn AuthenticationProvider>,
79 },
80 Onprem {
81 // TODO: cert paths?
82 provider: Option<OnpremAuthProvider>,
83 },
84 #[default]
85 None,
86}
87
88#[derive(Default, Debug, Clone, PartialEq)]
89pub(crate) enum AuthType {
90 File,
91 Instance,
92 Resource,
93 External,
94 Onprem,
95 Cloudsim,
96 #[default]
97 None,
98}
99
100/// The Oracle NoSQL Database mode to use.
101#[derive(Default, Debug, Clone, PartialEq)]
102pub enum HandleMode {
103 /// Connect to the Oracle NoSQL Cloud Service.
104 #[default]
105 Cloud,
106 /// Connect to a local Cloudsim instance (typically for testing purposes).
107 Cloudsim,
108 /// Connect to an on-premises installation of NoSQL Database Server.
109 Onprem,
110}
111
112impl HandleBuilder {
113 /// Create a new HandleBuilder struct.
114 ///
115 /// The default HandleBuilder does not set an authentication method. Consider calling
116 /// [`from_environment()`](HandleBuilder::from_environment()) to collect all parameters from
117 /// the local environment by default.
118 pub fn new() -> Self {
119 HandleBuilder {
120 ..Default::default()
121 }
122 }
123 /// Build a new [`Handle`].
124 ///
125 /// Note: Internally, if the [`HandleBuilder`] contains
126 /// a reference to an existing [`reqwest::Client`], it will clone and
127 /// use that. Otherwise, it will create a new [`reqwest::Client`] for its
128 /// own internal use. See [`reqwest_client()`](HandleBuilder::reqwest_client()).
129 pub async fn build(self) -> Result<Handle, NoSQLError> {
130 Handle::new(&self).await
131 }
132 /// Gather configuration settings from the current envrionment.
133 ///
134 /// This method will scan the process [`standard environment`](std::env::Vars) to collect and
135 /// set the configuration parameters. The values can be overridden in code if this method is
136 /// called first and other methods are called afterwards, for example:
137 ///```no_run
138 /// # use oracle_nosql_rust_sdk::Handle;
139 /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
140 /// let builder = Handle::builder()
141 /// .from_environment()?
142 /// .cloud_auth_from_file("~/nosql_oci_config")?;
143 /// # Ok(())
144 /// # }
145 ///```
146 /// The following environment variables are used:
147 ///
148 /// | variable | description |
149 /// | -------- | ----------- |
150 /// | `ORACLE_NOSQL_ENDPOINT` | The URL endpoint to use. See [`HandleBuilder::endpoint()`]. |
151 /// | `ORACLE_NOSQL_REGION` | The OCI region identifier. See [`HandleBuilder::cloud_region()`]. |
152 /// | `ORACLE_NOSQL_AUTH` | The auth mechanism. One of: `user`, `instance`, `resource`, `onprem`, `cloudsim`. |
153 /// | `ORACLE_NOSQL_AUTH_FILE` | For `user` auth, the path to the OCI config file (see [`HandleBuilder::cloud_auth_from_file()`]). For `onprem` auth, the path to the onprem user/password file (see [`HandleBuilder::onprem_auth_from_file()`]).
154 /// | `ORACLE_NOSQL_COMPARTMENT_ID` | For OCI auth, the default compartment id to use (see [`HandleBuilder::compartment_id()`]).
155 /// | `ORACLE_NOSQL_CA_CERT` | For `onprem` auth, the path to the certificate file in `pem` format (see [`HandleBuilder::add_cert_from_pemfile()`]). |
156 /// | `ORACLE_NOSQL_ACCEPT_INVALID_CERTS` | For `onprem` auth, if this is set to `1` or `true`, do not check certificates (see [`HandleBuilder::danger_accept_invalid_certs()`]). |
157 ///
158 pub fn from_environment(mut self) -> Result<Self, NoSQLError> {
159 self.from_environment = true;
160 let mut filename: Option<String> = None;
161 if let Some(val) = env::var("ORACLE_NOSQL_AUTH_FILE").ok() {
162 // TODO: verify file exists and is readable
163 filename = Some(val);
164 }
165 if let Some(val) = env::var("ORACLE_NOSQL_ENDPOINT").ok() {
166 self = self.endpoint(&val)?;
167 // TODO: parse region from endpoint?
168 }
169 if let Some(val) = env::var("ORACLE_NOSQL_REGION").ok() {
170 self = self.cloud_region(&val)?;
171 }
172 if let Some(val) = env::var("ORACLE_NOSQL_COMPARTMENT_ID").ok() {
173 self = self.compartment_id(&val)?;
174 }
175 if let Some(val) = env::var("ORACLE_NOSQL_CA_CERT").ok() {
176 self = self.add_cert_from_pemfile(&val)?;
177 }
178 if let Some(val) = env::var("ORACLE_NOSQL_ACCEPT_INVALID_CERTS").ok() {
179 let lv = val.to_lowercase();
180 if lv == "true" || lv == "1" {
181 self = self.danger_accept_invalid_certs(true)?;
182 }
183 }
184 if let Some(val) = env::var("ORACLE_NOSQL_AUTH").ok() {
185 let v = val.to_lowercase();
186 match v.as_str() {
187 "onprem" => {
188 // need user/pass?
189 if let Some(fname) = &filename {
190 self = self.onprem_auth_from_file(fname)?;
191 } else {
192 // TODO: error (need file)?
193 // Need a way to discern between insecure onprem and cloudsim
194 }
195 self.auth_type = AuthType::Onprem;
196 }
197 "resource" => self = self.cloud_auth_from_resource()?,
198 "instance" => self = self.cloud_auth_from_instance()?,
199 "user" => {
200 if let Some(fname) = &filename {
201 self = self.cloud_auth_from_file(fname)?;
202 } else {
203 self = self.cloud_auth_from_file("~/.oci/config")?;
204 }
205 }
206 "cloudsim" => {
207 self.mode = HandleMode::Cloudsim;
208 self.auth_type = AuthType::Cloudsim;
209 }
210 _ => {
211 return ia_err!("invalid value '{}' for ORACLE_NOSQL_AUTH", v);
212 }
213 }
214 }
215 Ok(self)
216 }
217 /// Set a specific endpoint connection to use.
218 ///
219 /// This is typically used when specifying a local cloudsim instance, or an
220 /// on-premises instance of the Oracle NoSQL Database Server. It can also be used to
221 /// override Cloud Service Region endpoints, or to specify an endpoint for a new
222 /// Region that has not been previously added to the SDK internally.
223 ///
224 /// Examples:
225 /// ```text
226 /// // Local cloudsim
227 /// http://localhost:8080
228 ///
229 /// // Local on-premises server
230 /// https://<database_host>:8080
231 ///
232 /// // Cloud service
233 /// https://nosql.us-ashburn-1.oci.oraclecloud.com
234 /// ```
235 pub fn endpoint(mut self, endpoint: &str) -> Result<Self, NoSQLError> {
236 // normalize to just domain[:port]
237 if endpoint.starts_with("https://") {
238 self.use_https = true;
239 let (_, b) = endpoint.split_at(8);
240 self.endpoint = b.to_string();
241 } else if endpoint.starts_with("http://") {
242 self.use_https = false;
243 let (_, b) = endpoint.split_at(7);
244 self.endpoint = b.to_string();
245 } else {
246 self.endpoint = endpoint.to_string();
247 }
248 Ok(self)
249 }
250 /// Set the mode for the handle.
251 ///
252 /// Use [`HandleMode::Cloudsim`] to specify connection to a local cloudsim instance.
253 ///
254 /// Use [`HandleMode::Onprem`] when connecting to an on-premises NoSQL Server.
255 ///
256 /// By default, HandleBuilder assumes [`HandleMode::Cloud`].
257 pub fn mode(mut self, mode: HandleMode) -> Result<Self, NoSQLError> {
258 self.mode = mode;
259 if self.mode == HandleMode::Cloudsim {
260 self.auth_type = AuthType::Cloudsim;
261 } else if self.mode == HandleMode::Onprem {
262 self.auth_type = AuthType::Onprem;
263 }
264 Ok(self)
265 }
266 #[doc(hidden)]
267 pub fn cloud_auth(
268 mut self,
269 provider: Box<dyn AuthenticationProvider>,
270 ) -> Result<Self, NoSQLError> {
271 // TODO: simple validation of provider?
272 let ap = AuthProvider::External { provider: provider };
273 self.auth = Arc::new(tokio::sync::Mutex::new(AuthConfig { provider: ap }));
274 self.use_https = true;
275 self.mode = HandleMode::Cloud;
276 self.auth_type = AuthType::External;
277 Ok(self)
278 }
279 /// Specify an OCI config file to use with user-based authentication.
280 ///
281 /// This method allows the use of a file other than the default `~/.oci/config` file.
282 /// See [SDK and CLI Configuration File](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm) for details.
283 /// This method assumes the use of the `"DEFAULT"` profile.
284 pub fn cloud_auth_from_file(self, config_file: &str) -> Result<Self, NoSQLError> {
285 self.cloud_auth_from_file_with_profile(config_file, "DEFAULT")
286 }
287 /// Specify an OCI config file to use with user-based authentication.
288 ///
289 /// This method allows the use of a file other than the default `~/.oci/config` file.
290 /// See [SDK and CLI Configuration File](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdkconfig.htm) for details.
291 pub fn cloud_auth_from_file_with_profile(
292 mut self,
293 config_file: &str,
294 profile: &str,
295 ) -> Result<Self, NoSQLError> {
296 let cfp = ConfigFileAuthenticationProvider::new_from_file(config_file, profile)?;
297 if self.region.is_none() && !cfp.region_id().is_empty() {
298 self = self.cloud_region(cfp.region_id())?;
299 }
300 let ap = AuthProvider::File {
301 //path: config_file.to_string(),
302 //profile: profile.to_string(),
303 provider: Box::new(cfp),
304 };
305 self.auth = Arc::new(tokio::sync::Mutex::new(AuthConfig { provider: ap }));
306 self.auth_type = AuthType::File;
307 self.use_https = true;
308 self.mode = HandleMode::Cloud;
309 Ok(self)
310 }
311 // TODO: cloud_auth_from_session
312 /// Specify using OCI Instance Principal for authentication.
313 ///
314 /// Instance Principal is an IAM service feature that enables instances to be authorized actors (or _principals_) to perform actions on service resources.
315 /// If the application is running on an OCI compute instance in the Oracle Cloud,
316 /// the SDK can make use of the instance environment to determine its credentials (no config file is required).
317 /// Each compute instance has its own identity, and it authenticates using the certificates that are added to it.
318 /// See [Calling Services from an Instance](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm) for prerequisite steps to set up Instance Principal.
319 ///
320 pub fn cloud_auth_from_instance(mut self) -> Result<Self, NoSQLError> {
321 //let ifp = InstancePrincipalAuthProvider::new().await?;
322 //let ap = AuthProvider::Instance {
323 //provider: Box::new(ifp),
324 //};
325 //self.auth = Arc::new(tokio::sync::Mutex::new(AuthConfig { provider: ap }));
326 self.auth_type = AuthType::Instance;
327 self.use_https = true;
328 self.mode = HandleMode::Cloud;
329 Ok(self)
330 }
331 // TODO: cloud_auth_from_oke
332 /// Specify using OCI Resource Principal for authentication.
333 ///
334 /// Resource Principal is an IAM service feature that enables the resources to be authorized actors
335 /// (or _principals_) to perform actions on service resources. You may use Resource Principal when calling
336 /// Oracle NoSQL Database Cloud Service from other Oracle Cloud service resources such as
337 /// [Functions](https://docs.cloud.oracle.com/en-us/iaas/Content/Functions/Concepts/functionsoverview.htm).
338 /// See [Accessing Other Oracle Cloud Infrastructure Resources from Running Functions](https://docs.cloud.oracle.com/en-us/iaas/Content/Functions/Tasks/functionsaccessingociresources.htm) for how to set up Resource Principal.
339 pub fn cloud_auth_from_resource(mut self) -> Result<Self, NoSQLError> {
340 //let rfp = ResourcePrincipalAuthProvider::new()?;
341 //let ap = AuthProvider::Resource {
342 //provider: Box::new(rfp),
343 //};
344 //self.auth = Arc::new(tokio::sync::Mutex::new(AuthConfig { provider: ap }));
345 self.auth_type = AuthType::Resource;
346 self.use_https = true;
347 self.mode = HandleMode::Cloud;
348 Ok(self)
349 }
350 /// Specify a region identifier for the NoSQL Cloud Service.
351 ///
352 /// This method is only required if using cloud user file-based authentication and the
353 /// given config file does not have a `region` specification. The value should be a
354 /// cloud-standard identifier for the region, such as `us-ashburn-1`. For more information
355 /// on regions, see [Regions and Availability Domains](https://docs.cloud.oracle.com/en-us/iaas/Content/General/Concepts/regions.htm).
356 ///
357 /// The NoSQL rust SDK maintains an internal list of regions where the NoSQL service is available.
358 /// The region identifier passed to this method is validated against the internal list. If the region
359 /// identifier is not found, it is then compared to the region metadata contained in the `OCI_REGION_METADATA`
360 /// environment variable (if set), and to region metadata that may exist in a `~/.oci/regions-config.json` file.
361 /// See [Adding Regions](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/sdk_adding_new_region_endpoints.htm) for details of these settings. In this way, new regions where NoSQL has been added may
362 /// be used without needing to update to the latest NoSQL rust SDK.
363 pub fn cloud_region(mut self, region: &str) -> Result<Self, NoSQLError> {
364 let r = string_to_region(region)?;
365 if self.endpoint.is_empty() {
366 self.endpoint = r.nosql_endpoint();
367 }
368 self.region = Some(r);
369 self.use_https = true;
370 self.mode = HandleMode::Cloud;
371 Ok(self)
372 }
373 /// Cloud Service only: set the name or id of a compartment to be used for all operations.
374 ///
375 /// If the associated handle authenticated as an Instance Principal, this value must be an OCID.
376 /// In all other cases, the value may be specified as either a name (or path for nested compartments) or as an OCID.
377 ///
378 /// This value may be overridden on a per-request basis.
379 ///
380 /// If no compartment is given, the root compartment of the tenancy will be used.
381 pub fn compartment_id(mut self, compartment_id: &str) -> Result<Self, NoSQLError> {
382 self.default_compartment_id = compartment_id.to_string();
383 Ok(self)
384 }
385 /// Specify credentials for use with a secure On-premises NoSQL Server.
386 ///
387 /// When using a secure server, a username and password are required. Use this method
388 /// to specify the values.
389 ///
390 /// Calling this method will also internally set the `HandleMode` to `Onprem`.
391 pub fn onprem_auth(mut self, username: &str, passwd: &str) -> Result<Self, NoSQLError> {
392 if !username.is_empty() {
393 let ap = AuthProvider::Onprem {
394 provider: Some(OnpremAuthProvider::new(&self, username, passwd)),
395 };
396 self.auth = Arc::new(tokio::sync::Mutex::new(AuthConfig { provider: ap }));
397 } else {
398 let ap = AuthProvider::Onprem { provider: None };
399 self.auth = Arc::new(tokio::sync::Mutex::new(AuthConfig { provider: ap }));
400 }
401 self.mode = HandleMode::Onprem;
402 self.auth_type = AuthType::Onprem;
403 Ok(self)
404 }
405 /// Specify credentials for use with a secure On-premises NoSQL Server from a local file.
406 ///
407 /// When using a secure server, a username and password are required. Use this method
408 /// to specify the values from a file. The format of the file is one value per line, using
409 /// a `key=value` pair syntax, such as:
410 ///```text
411 /// username=testuser
412 /// password=1234567
413 ///```
414 ///
415 /// Calling this method will also internally set the `HandleMode` to `Onprem`.
416 pub fn onprem_auth_from_file(mut self, filename: &str) -> Result<Self, NoSQLError> {
417 // read user/pass from file
418 let mut user = "".to_string();
419 let mut pass = "".to_string();
420 let data = file_to_string(filename)?;
421 // format: one k/v per line, k=v pairs
422 for line in data.split("\n") {
423 if let Some((k, v)) = line.split_once("=") {
424 if k == "username" {
425 user = v.to_string();
426 } else if k == "password" {
427 pass = v.to_string();
428 }
429 }
430 }
431 // TODO: is password a required field?
432 if user.is_empty() {
433 return ia_err!("username field missing from onprem auth file {}", filename);
434 }
435 self = self.onprem_auth(&user, &pass)?;
436 self.use_https = true;
437 Ok(self)
438 }
439 /// Add a certificate to use for on-premises https connections from a file.
440 ///
441 /// The file must contain an x509 certificate in `PEM` file format.
442 pub fn add_cert_from_pemfile(self, pemfile: &str) -> Result<Self, NoSQLError> {
443 let buf = file_to_string(pemfile)?.into_bytes();
444 match reqwest::Certificate::from_pem(&buf) {
445 Ok(cert) => {
446 return self.add_cert(cert);
447 }
448 Err(e) => {
449 return ia_err!(
450 "error getting certificate from pemfile {}: {}",
451 pemfile,
452 e.to_string()
453 );
454 }
455 }
456 }
457
458 /// Add a certificate to use for on-premises https connections.
459 pub fn add_cert(mut self, cert: Certificate) -> Result<Self, NoSQLError> {
460 self.add_cert = Some(cert);
461 Ok(self)
462 }
463 // see https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.danger_accept_invalid_certs
464 /// Allow https connection without validating certificates.
465 ///
466 /// **Warning:** This is only recommended for local testing purposes. Its use is insecure. See [`reqwest::ClientBuilder::danger_accept_invalid_certs()`] for details.
467 ///
468 pub fn danger_accept_invalid_certs(
469 mut self,
470 accept_invalid_certs: bool,
471 ) -> Result<Self, NoSQLError> {
472 self.accept_invalid_certs = accept_invalid_certs;
473 Ok(self)
474 }
475 /// Specify a [`reqwest::Client`] to use for all http/s connections.
476 ///
477 /// By default, the [`NoSQL Handle`](crate::Handle) creates an internal [`reqwest::Client`] to use for
478 /// all communications. If your application already has a reqwest Client, you can pass that
479 /// into the HandleBuilder to avoid creating multiple connection pools.
480 pub fn reqwest_client(mut self, client: &Client) -> Result<Self, NoSQLError> {
481 // TODO: validate client is open/operational?
482 self.client = Some(client.clone());
483 Ok(self)
484 }
485 /// Specify the timeout used for operations.
486 ///
487 /// Currently this is used for both connection and request timeouts.
488 /// Note that the request timeout can be set on a per-request basis.
489 ///
490 /// The default timeout is 30 seconds.
491 pub fn timeout(mut self, timeout: Duration) -> Result<Self, NoSQLError> {
492 // TODO: validate timeout
493 self.timeout = Some(timeout);
494 Ok(self)
495 }
496
497 // for doc testing use only
498 #[doc(hidden)]
499 pub fn in_test(mut self, in_test: bool) -> Self {
500 self.in_test = in_test;
501 self
502 }
503
504 // Return true if the auth has been updated/refreshed.
505 // Return false if there are no errors, but nothing was refreshed.
506 pub(crate) async fn refresh_auth(&self, client: &Client) -> Result<bool, NoSQLError> {
507 // It is safe to keep this mutex lock across await because we're using tokio::sync::Mutex
508 let mut pguard = self.auth.lock().await;
509 match &mut pguard.provider {
510 AuthProvider::Instance { provider: _ } => {
511 // create an entirely new IP auth, as currently IP Auth has no methods to refresh itself
512 let ifp = InstancePrincipalAuthProvider::new().await?;
513 pguard.provider = AuthProvider::Instance {
514 provider: Box::new(ifp),
515 };
516 return Ok(true);
517 }
518 AuthProvider::Onprem { provider } => {
519 if let Some(prov) = provider {
520 let _ = prov.generate_token(client, true).await?;
521 }
522 }
523 // TODO: maybe refresh file-based auth?
524 _ => {}
525 }
526 Ok(false)
527 }
528}
529
530// On premises auth
531#[derive(Default, Debug, Clone)]
532pub(crate) struct OnpremAuthProvider {
533 pub(crate) inner: Arc<OnpremAuthProviderRef>,
534}
535
536#[derive(Default, Debug)]
537pub(crate) struct OnpremAuthProviderRef {
538 username: String,
539 password: String,
540 endpoint: String,
541 // We use a tokio Mutex because we occasionally hold a lock across awaits
542 token: tokio::sync::Mutex<OnpremToken>,
543}
544
545#[derive(Default, Debug, Deserialize)]
546struct OnpremToken {
547 token: String,
548 #[serde(rename = "expireAt")]
549 expire_at: i64,
550}
551
552impl OnpremAuthProvider {
553 pub fn new(builder: &HandleBuilder, user: &str, pass: &str) -> OnpremAuthProvider {
554 // normalize endpoint to "http[s]://{endpoint}/V2/nosql/security"
555 let mut ep = String::from("http");
556 if builder.use_https {
557 ep.push('s');
558 }
559 ep.push_str("://");
560 ep.push_str(&builder.endpoint);
561 ep.push_str("/V2/nosql/security");
562 OnpremAuthProvider {
563 inner: Arc::new(OnpremAuthProviderRef {
564 username: user.to_string(),
565 password: pass.to_string(),
566 endpoint: ep,
567 token: tokio::sync::Mutex::new(OnpremToken::default()),
568 }),
569 }
570 // TODO: should new() attempt to connect to the service? Or wait until
571 // first time needing headers?
572 }
573 pub async fn add_required_headers(
574 &self,
575 client: &Client,
576 headers: &mut HeaderMap,
577 ) -> Result<(), NoSQLError> {
578 let mut bearer = self.generate_token(client, false).await?;
579 bearer.insert_str(0, "Bearer ");
580 headers.insert("Authorization", HeaderValue::from_str(&bearer)?);
581 Ok(())
582 }
583 async fn generate_token(&self, client: &Client, force: bool) -> Result<String, NoSQLError> {
584 let mut tguard = self.inner.token.lock().await;
585 if !force && !tguard.token.is_empty() && (tguard.expire_at - 10000) > Self::now() {
586 return Ok(tguard.token.clone());
587 }
588
589 let mut headers = HeaderMap::new();
590 headers.insert("Accept", HeaderValue::from_str("application/json")?);
591 let mut ep = self.inner.endpoint.clone();
592 let bup = {
593 if tguard.token.is_empty() {
594 ep.push_str("/login");
595 // set Authorization: Basic base64(username:password)
596 let up = format!("{}:{}", &self.inner.username, &self.inner.password);
597 format!("Basic {}", BASE64_STANDARD.encode(up))
598 } else {
599 ep.push_str("/renew");
600 // set Authorization: Bearer {token}
601 format!("Bearer {}", tguard.token)
602 }
603 };
604 headers.insert("Authorization", HeaderValue::from_str(&bup)?);
605 let resp = client.get(ep).headers(headers).send().await?;
606 // parse returned JSON
607 let result = resp.text().await?;
608 let nt: Result<OnpremToken, serde_json::Error> = serde_json::from_str(&result);
609 if let Ok(new_token) = nt {
610 tguard.token = new_token.token;
611 tguard.expire_at = new_token.expire_at;
612 return Ok(tguard.token.clone());
613 }
614 ia_err!("error from onprem login service: {}", result)
615 }
616
617 fn now() -> i64 {
618 let umillis = SystemTime::now()
619 .duration_since(UNIX_EPOCH)
620 .unwrap()
621 .as_millis();
622 let imillis: i64 = umillis.try_into().unwrap();
623 imillis
624 }
625}