use std::{
env,
error::Error,
fmt::{Display, Formatter},
process::Command,
};
use async_recursion::async_recursion;
use azure_core::credentials::TokenCredential;
use azure_identity::DeveloperToolsCredential;
use reqwest::Client;
use crate::model::account::AccountsResponse;
use crate::model::profile::Profile;
const DEVOPS_RESOURCE: &str = "499b84ac-1321-427f-aa17-267ca6975798/.default";
const PROFILE_API_URL: &str = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me";
const ACCOUNTS_API_URL: &str = "https://app.vssps.visualstudio.com/_apis/accounts";
const ACCOUNTS_API_VERSION: &str = "7.1";
#[derive(Debug)]
pub struct AzureADToken(String);
impl Display for AzureADToken {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0)
}
}
#[async_recursion]
pub async fn get_ad_token_for_devops(tries: i8) -> Result<AzureADToken, Box<dyn Error>> {
let res = DeveloperToolsCredential::new(None)?
.get_token(&[DEVOPS_RESOURCE], None)
.await;
match res {
Ok(token_response) => Ok(AzureADToken(token_response.token.secret().to_string())),
Err(e) => {
if tries > 0 {
println!("🔐 No credential available. Trying to log in with az cli...");
Command::new("az")
.args(vec!["login"])
.output()
.expect("Login failed.");
get_ad_token_for_devops(tries - 1).await
} else {
Err::<AzureADToken, Box<dyn Error>>(Box::new(e))
}
}
}
}
pub async fn get_user_profile(token: &AzureADToken) -> Result<Profile, Box<dyn Error>> {
let client = Client::new();
let response = client
.get(PROFILE_API_URL)
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.query(&[("api-version", ACCOUNTS_API_VERSION)])
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Failed to get user profile: {}", response.status()).into());
}
let profile = response.json::<Profile>().await?;
Ok(profile)
}
pub async fn list_organizations(token: &AzureADToken) -> Result<Vec<String>, Box<dyn Error>> {
let profile = get_user_profile(token).await?;
let client = Client::new();
let response = client
.get(ACCOUNTS_API_URL)
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.query(&[
("memberId", profile.id.as_str()),
("api-version", ACCOUNTS_API_VERSION),
])
.send()
.await?;
if !response.status().is_success() {
return Err(format!("Failed to list organizations: {}", response.status()).into());
}
let accounts_response = response.json::<AccountsResponse>().await?;
Ok(accounts_response
.value
.into_iter()
.map(|account| account.account_name)
.collect())
}
pub async fn get_organization(token: &AzureADToken) -> Result<String, Box<dyn Error>> {
let orgs = list_organizations(token).await?;
if orgs.is_empty() {
return Err("No Azure DevOps organizations found for the authenticated user".into());
}
if let Ok(org) = env::var("DEVOPS_ORGANIZATION")
&& !org.is_empty()
{
if orgs.contains(&org) {
return Ok(org);
} else {
return Err(format!(
"Organization '{}' from DEVOPS_ORGANIZATION environment variable is not accessible to the authenticated user. Available organizations: {}",
org,
orgs.join(", ")
).into());
}
}
match orgs.len() {
0 => Err("No Azure DevOps organizations found for the authenticated user".into()),
1 => Ok(orgs[0].clone()),
_ => Err(format!(
"Multiple Azure DevOps organizations found: {}. Please set the DEVOPS_ORGANIZATION environment variable to specify which one to use.",
orgs.join(", ")
).into()),
}
}