cloud_terrastodon_azure 0.35.1

Helpers for interacting with Azure for the Cloud Terrastodon project
use crate::fetch_all_entra_users;
use crate::fetch_all_service_principals;
use crate::fetch_oauth2_permission_grants;
use cloud_terrastodon_azure_types::AzureTenantId;
use cloud_terrastodon_azure_types::ConsentType;
use cloud_terrastodon_azure_types::EntraServicePrincipal;
use cloud_terrastodon_azure_types::EntraUser;
use cloud_terrastodon_azure_types::OAuth2PermissionGrant;
use cloud_terrastodon_user_input::Choice;
use cloud_terrastodon_user_input::PickerTui;
use eyre::bail;
use itertools::Itertools;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt;
use std::future::IntoFuture;
use tokio::try_join;

#[derive(Debug)]
pub struct Grant {
    pub grant: OAuth2PermissionGrant,
    pub service_principal: EntraServicePrincipal,
    pub target: Target,
}
#[derive(Debug)]
pub enum Target {
    AllPrincipals,
    User(Box<EntraUser>),
}

impl PartialEq for Grant {
    fn eq(&self, other: &Self) -> bool {
        self.grant.client_id == other.grant.client_id
            && self.grant.principal_id == other.grant.principal_id
    }
}

impl Eq for Grant {}

impl PartialOrd for Grant {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Grant {
    fn cmp(&self, other: &Self) -> Ordering {
        self.grant
            .client_id
            .cmp(&other.grant.client_id)
            .then_with(
                || match (&self.grant.principal_id, &other.grant.principal_id) {
                    (Some(a), Some(b)) => a.cmp(b),
                    (None, None) => Ordering::Equal,
                    (Some(_), None) => Ordering::Greater,
                    (None, Some(_)) => Ordering::Less,
                },
            )
    }
}

impl fmt::Display for Grant {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}\n| {}\n| {}",
            self.service_principal.display_name,
            match &self.target {
                Target::User(user) => format!("User ({})", user.user_principal_name),
                x => format!("{x:?}"),
            },
            self.grant.scope.trim()
        )
    }
}

pub async fn pick_oauth2_permission_grants(tenant_id: AzureTenantId) -> eyre::Result<Vec<Grant>> {
    let grants = fetch_oauth2_permission_grants(tenant_id);
    let service_principals = fetch_all_service_principals(tenant_id);
    let users = fetch_all_entra_users(tenant_id).into_future();
    let (grants, service_principals, users) = try_join!(grants, service_principals, users)?;
    let service_principals_map = service_principals
        .iter()
        .map(|x| (&x.id, x))
        .collect::<HashMap<_, _>>();
    let users_map = users.iter().map(|x| (&x.id, x)).collect::<HashMap<_, _>>();

    let grants = grants
        .into_iter()
        .map(|grant| {
            let Some(service_principal) = service_principals_map.get(&grant.client_id) else {
                bail!(
                    "Failed to find service principal with id {:?} for grant {:?}",
                    &grant.client_id,
                    grant
                );
            };
            Ok(match (&grant.consent_type, &grant.principal_id) {
                (ConsentType::AllPrincipals, None) => Grant {
                    grant,
                    service_principal: (*service_principal).clone(),
                    target: Target::AllPrincipals,
                },
                (ConsentType::Principal, Some(user_id)) => {
                    let Some(user) = users_map.get(&user_id.clone()) else {
                        bail!("User not found with id {} for grant {:?}", user_id, grant);
                    };
                    Grant {
                        grant,
                        service_principal: (*service_principal).clone(),
                        target: Target::User(Box::new((*user).clone())),
                    }
                }
                _ => bail!(
                    "Invalid state: consent type inconsistent with principal id for {:?}",
                    grant
                ),
            })
        })
        .collect::<eyre::Result<Vec<Grant>>>()?;
    let choices = grants.into_iter().sorted_unstable().map(|g| Choice {
        key: g.to_string(),
        value: g,
    });
    let chosen = PickerTui::new()
        .set_header("Pick the items to browse")
        .pick_many(choices)?;
    Ok(chosen)
}