1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
use anyhow::{bail, Result};
use reqwest::blocking::Client;
use serde::Serialize;
use serde_json::Value;

#[derive(Serialize, PartialOrd, Ord, PartialEq, Eq)]
pub struct ScopeEntry {
    pub role: String,
    pub scope: String,
    pub scope_name: String,
    #[serde(skip)]
    pub role_definition_id: String,
}

impl ScopeEntry {
    // NOTE: serde_json doesn't panic on failed index slicing, it returns a Value
    // that allows further nested nulls
    #[allow(clippy::indexing_slicing)]
    fn parse(body: &Value) -> Result<Vec<Self>> {
        let Some(values) = body["value"].as_array() else {
            bail!("unable to parse response: missing value array: {body:#?}");
        };

        let mut results = Vec::new();
        for entry in values {
            let Some(role) =
                entry["properties"]["expandedProperties"]["roleDefinition"]["displayName"].as_str()
            else {
                bail!("no role name: {entry:#?}");
            };

            let Some(scope) = entry["properties"]["expandedProperties"]["scope"]["id"].as_str()
            else {
                bail!("no scope id: {entry:#?}");
            };

            let Some(scope_name) =
                entry["properties"]["expandedProperties"]["scope"]["displayName"].as_str()
            else {
                bail!("no scope name: {entry:#?}");
            };

            let Some(role_definition_id) = entry["properties"]["roleDefinitionId"].as_str() else {
                bail!("no role definition id: {entry:#?}");
            };

            results.push(Self {
                role: role.to_string(),
                scope: scope.to_string(),
                scope_name: scope_name.to_string(),
                role_definition_id: role_definition_id.to_string(),
            });
        }
        results.sort();
        Ok(results)
    }
}

/// List the roles available to the current user
///
/// # Errors
/// Will return `Err` if the request fails or the response is not valid JSON
pub fn list_roles(token: &str) -> Result<Vec<ScopeEntry>> {
    let url = "https://management.azure.com/providers/Microsoft.Authorization/roleEligibilityScheduleInstances";
    let response = Client::new()
        .get(url)
        .query(&[("$filter", "asTarget()"), ("api-version", "2020-10-01")])
        .bearer_auth(token)
        .send()?
        .error_for_status()?;
    ScopeEntry::parse(&response.json()?)
}