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}