use silicon_iam_client::{Client, Credential, EnvironmentKey, Mutation};
use uuid::Uuid;
use crate::{
error::{CliError, Result},
output::Format,
store::{self, Profile, Session},
};
pub const DEFAULT_PROFILE: &str = "default";
pub const DEFAULT_URL: &str = "https://backend.iam.teamofsilicons.com";
pub struct Context {
pub format: Format,
pub profile_name: String,
pub profile: Profile,
pub step_up: Option<String>,
organization: Option<String>,
testing_environment_id: Option<Uuid>,
client: Client,
}
impl Context {
pub fn new(
format: Format,
profile: Option<String>,
url: Option<String>,
organization: Option<String>,
testing_environment_id: Option<Uuid>,
step_up: Option<String>,
) -> Result<Self> {
let config = store::load_config()?;
let profile_name = profile
.or_else(|| std::env::var("SILICON_IAM_PROFILE").ok())
.or_else(|| config.current_profile.clone())
.unwrap_or_else(|| DEFAULT_PROFILE.to_owned());
let mut stored = config
.profiles
.get(&profile_name)
.cloned()
.unwrap_or_default();
if stored.url.is_empty() {
DEFAULT_URL.clone_into(&mut stored.url);
}
if let Some(url) = url.or_else(|| std::env::var("SILICON_IAM_URL").ok()) {
stored.url = url;
}
let organization = organization
.or_else(|| std::env::var("SILICON_IAM_ORG").ok())
.or_else(|| stored.org.clone());
let mut builder =
Client::builder(&stored.url)?.user_agent(concat!("iam/", env!("CARGO_PKG_VERSION")));
if let Some(environment_id) = testing_environment_id {
let credentials = store::load_credentials()?;
let Some(key) = credentials.testing_environment_key(&profile_name, environment_id)
else {
return Err(CliError::UnknownTestingEnvironment(environment_id));
};
builder = builder.environment(EnvironmentKey::new(key.to_owned())?);
}
Ok(Self {
format,
profile_name,
profile: stored,
step_up,
organization,
testing_environment_id,
client: builder.build()?,
})
}
#[must_use]
pub const fn anonymous(&self) -> &Client {
&self.client
}
#[must_use]
pub const fn testing_environment_id(&self) -> Option<Uuid> {
self.testing_environment_id
}
pub fn require_test(&self) -> Result<Uuid> {
self.testing_environment_id
.ok_or(CliError::TestEnvironmentRequired)
}
pub async fn authenticated(&self) -> Result<Client> {
let session = self.session()?;
let session = if session.needs_refresh() {
self.renew(&session).await?
} else {
session
};
Ok(self
.client
.with_credential(Credential::bearer(session.access_token)))
}
pub fn session(&self) -> Result<Session> {
store::load_credentials()?
.session(&self.profile_name, self.testing_environment_id)
.cloned()
.ok_or(CliError::NotSignedIn)
}
pub fn remember(&self, session: Session) -> Result<()> {
let mut credentials = store::load_credentials()?;
credentials.set_session(&self.profile_name, self.testing_environment_id, session);
store::save_credentials(&credentials)
}
pub fn forget(&self) -> Result<bool> {
let mut credentials = store::load_credentials()?;
let existed = credentials.remove_session(&self.profile_name, self.testing_environment_id);
store::save_credentials(&credentials)?;
Ok(existed)
}
pub fn remember_testing_environment(&self, environment_id: Uuid, key: String) -> Result<()> {
let mut credentials = store::load_credentials()?;
credentials.set_testing_environment_key(&self.profile_name, environment_id, key);
store::save_credentials(&credentials)
}
pub fn organization(&self) -> Result<&str> {
self.organization.as_deref().ok_or(CliError::NoOrganization)
}
pub fn organization_or<'a>(&'a self, explicit: Option<&'a str>) -> Result<&'a str> {
match explicit {
Some(org) => Ok(org),
None => self.organization(),
}
}
#[must_use]
pub fn mutation(&self) -> Mutation {
let mutation = Mutation::new();
match &self.step_up {
Some(token) => mutation.step_up(token.clone()),
None => mutation,
}
}
async fn renew(&self, session: &Session) -> Result<Session> {
let tokens = self
.client
.auth()
.refresh(&session.refresh_token, &Mutation::new())
.await?;
let renewed = crate::commands::auth::session_from(&tokens, &session.carbon_id);
self.remember(renewed.clone())?;
Ok(renewed)
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::{Context, DEFAULT_PROFILE, DEFAULT_URL};
use crate::{error::CliError, output::Format, store::Profile};
fn context(testing_environment_id: Option<Uuid>) -> Context {
let Ok(client) = silicon_iam_client::Client::new(DEFAULT_URL) else {
panic!("the default URL must build");
};
Context {
format: Format::Text,
profile_name: DEFAULT_PROFILE.to_owned(),
profile: Profile {
url: DEFAULT_URL.to_owned(),
org: None,
},
step_up: None,
organization: None,
testing_environment_id,
client,
}
}
#[test]
fn test_only_actions_fail_clearly_without_test_context() {
assert!(matches!(
context(None).require_test(),
Err(CliError::TestEnvironmentRequired)
));
let id = Uuid::from_u128(17);
assert_eq!(context(Some(id)).require_test().ok(), Some(id));
}
}