use std::time::Duration;
use dynamic_config::Error;
use dynamic_config_store_core::credential::{Cached, Issued};
const METADATA_TOKEN_URL: &str =
"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
#[derive(Clone)]
#[non_exhaustive]
pub enum Auth {
Emulator,
AccessToken(String),
MetadataServer {
url: String,
},
}
impl Auth {
pub fn access_token(token: impl Into<String>) -> Self {
Self::AccessToken(token.into())
}
#[must_use]
pub fn metadata_server() -> Self {
Self::MetadataServer {
url: METADATA_TOKEN_URL.to_owned(),
}
}
#[must_use]
pub fn with_url(mut self, url: impl Into<String>) -> Self {
if let Self::MetadataServer { url: existing } = &mut self {
*existing = url.into();
}
self
}
}
impl std::fmt::Debug for Auth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Emulator => f.write_str("Emulator"),
Self::AccessToken(_) => f.write_str("AccessToken(***)"),
Self::MetadataServer { url } => {
f.debug_struct("MetadataServer").field("url", url).finish()
}
}
}
}
#[derive(Debug, Default)]
pub(crate) struct Session {
token: Cached<String>,
}
impl Session {
pub(crate) const fn new() -> Self {
Self {
token: Cached::new(),
}
}
pub(crate) fn token(&self, auth: &Auth, agent: &ureq::Agent) -> Result<Option<String>, Error> {
match auth {
Auth::Emulator => Ok(None),
Auth::AccessToken(token) => Ok(Some(token.clone())),
Auth::MetadataServer { url } => self
.token
.get(|_previous| Self::mint(url, agent))
.map(Some),
}
}
fn mint(url: &str, agent: &ureq::Agent) -> Result<Issued<String>, Error> {
let response: serde_json::Value = agent
.get(url)
.header("Metadata-Flavor", "Google")
.call()
.map_err(|error| {
let described = format!("firestore: the metadata server: {error}");
match error {
ureq::Error::StatusCode(401 | 403) => Error::auth(described),
_ => Error::remote(described),
}
})?
.body_mut()
.read_json()
.map_err(|error| {
Error::remote(format!(
"firestore: the metadata server's response was not JSON: {error}"
))
})?;
let secret = response
.get("access_token")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
Error::remote("firestore: the metadata server returned no `access_token`")
})?
.to_owned();
let ttl = response
.get("expires_in")
.and_then(serde_json::Value::as_u64)
.filter(|seconds| *seconds > 0)
.map(Duration::from_secs);
Ok(Issued { value: secret, ttl })
}
pub(crate) fn invalidate(&self) {
self.token.invalidate();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_emulator_presents_nothing() {
let agent = ureq::Agent::new_with_defaults();
assert!(Session::new()
.token(&Auth::Emulator, &agent)
.unwrap()
.is_none());
}
#[test]
fn a_supplied_token_is_presented_as_it_is() {
let agent = ureq::Agent::new_with_defaults();
assert_eq!(
Session::new()
.token(&Auth::access_token("ya29.abc"), &agent)
.unwrap()
.as_deref(),
Some("ya29.abc")
);
}
#[test]
fn the_metadata_url_can_be_moved_for_a_sidecar() {
let auth = Auth::metadata_server().with_url("http://127.0.0.1:8081/token");
let Auth::MetadataServer { url } = auth else {
panic!("still a metadata auth");
};
assert_eq!(url, "http://127.0.0.1:8081/token");
}
}