diff --git a/README.md b/README.md index a70d29d..2d3da7c 100644 --- a/README.md +++ b/README.md @@ -425,6 +425,7 @@ Usage: role [OPTIONS] Commands: assignment Manage role assignments definition Manage role definitions + resources Commands related to resources in Azure Options: --verbose... @@ -768,6 +769,79 @@ $ az-pim role definition list --subscription 00000000-0000-0000-0000-00000000000 $ ``` +### az-pim role resources + +``` +Commands related to resources in Azure + +Usage: resources [OPTIONS] + +Commands: + list List the child resources of a resource which you have eligible access + +Options: + --verbose... + Increase logging verbosity. Provide repeatedly to increase the verbosity + + --quiet + Only show errors + + -h, --help + Print help + +``` +#### az-pim role resources list + +``` +List the child resources of a resource which you have eligible access + +Usage: list [OPTIONS] + +Options: + --subscription + Limit the scope by the specified Subscription + + --verbose... + Increase logging verbosity. Provide repeatedly to increase the verbosity + + --quiet + Only show errors + + --resource-group + Limit the scope by the specified Resource Group + + This argument requires `subscription` to be set. + + --provider + Provider + + This argument requires `subscription` and `resource_group` to be set. + + --scope + Specify scope directly + + -h, --help + Print help (see a summary with '-h') + +``` +##### Example Usage + +``` +$ az-pim role resources list --subscription 00000000-0000-0000-0000-000000000000 +[ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/DefaultResourceGroup-EUS", + "name": "DefaultResourceGroup-EUS", + "type": "resourcegroup" + }, + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/DefaultResourceGroup-SUK", + "name": "DefaultResourceGroup-SUK", + "type": "resourcegroup" + } +] +``` + ## az-pim init ``` diff --git a/src/bin/az-pim.rs b/src/bin/az-pim.rs index 2e9d844..c66c154 100644 --- a/src/bin/az-pim.rs +++ b/src/bin/az-pim.rs @@ -58,7 +58,8 @@ impl Cmd { | "az-pim deactivate interactive" | "az-pim role" | "az-pim role assignment" - | "az-pim role definition" => None, + | "az-pim role definition" + | "az-pim role resources" => None, "az-pim list" => Some(include_str!("../help/az-pim-list.txt")), "az-pim activate role " => { Some(include_str!("../help/az-pim-activate-role.txt")) @@ -86,6 +87,9 @@ impl Cmd { "az-pim role definition list" => { Some(include_str!("../help/az-pim-role-definition-list.txt")) } + "az-pim role resources list" => { + Some(include_str!("../help/az-pim-role-resources-list.txt")) + } unsupported => unimplemented!("unable to generate example for {unsupported}"), } } @@ -449,6 +453,12 @@ enum RoleSubCommand { #[clap(subcommand)] cmd: DefinitionSubCommand, }, + + /// Commands related to resources in Azure + Resources { + #[clap(subcommand)] + cmd: ResourcesSubCommand, + }, } #[derive(Subcommand)] @@ -678,6 +688,50 @@ impl DefinitionSubCommand { } } +#[derive(Subcommand)] +enum ResourcesSubCommand { + /// List the child resources of a resource which you have eligible access + List { + /// Limit the scope by the specified Subscription + #[arg(long)] + subscription: Option, + + /// Limit the scope by the specified Resource Group + /// + /// This argument requires `subscription` to be set. + #[arg(long, requires = "subscription")] + resource_group: Option, + + /// Provider + /// + /// This argument requires `subscription` and `resource_group` to be set. + #[arg(long, requires = "resource_group")] + provider: Option, + + /// Specify scope directly + #[arg(long, conflicts_with = "subscription", required_unless_present_any = ["subscription", "resource_group", "provider"])] + scope: Option, + }, +} + +impl ResourcesSubCommand { + fn run(self, client: &PimClient) -> Result<()> { + match self { + Self::List { + subscription, + resource_group, + scope, + provider, + } => { + let scope = build_scope(subscription, resource_group, scope, provider)? + .context("valid scope must be provided")?; + output(&client.eligible_child_resources(&scope)?)?; + } + } + Ok(()) + } +} + /// Parse a single key-value pair of `X=Y` into a typed tuple of `(X, Y)`. /// /// # Errors @@ -816,6 +870,7 @@ fn main() -> Result<()> { SubCommand::Role { cmd } => match cmd { RoleSubCommand::Assignment { cmd } => cmd.run(&client), RoleSubCommand::Definition { cmd } => cmd.run(&client), + RoleSubCommand::Resources { cmd } => cmd.run(&client), }, SubCommand::Readme => { build_readme(); diff --git a/src/help/az-pim-role-resources-list.txt b/src/help/az-pim-role-resources-list.txt new file mode 100644 index 0000000..df6290e --- /dev/null +++ b/src/help/az-pim-role-resources-list.txt @@ -0,0 +1,13 @@ +$ az-pim role resources list --subscription 00000000-0000-0000-0000-000000000000 +[ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/DefaultResourceGroup-EUS", + "name": "DefaultResourceGroup-EUS", + "type": "resourcegroup" + }, + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/DefaultResourceGroup-SUK", + "name": "DefaultResourceGroup-SUK", + "type": "resourcegroup" + } +] \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index be977f3..f38bc68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ mod definitions; mod graph; pub mod interactive; mod latest; +pub mod resources; pub mod roles; pub mod scope; @@ -26,6 +27,7 @@ use crate::{ backend::Backend, definitions::{Definition, Definitions}, graph::get_objects_by_ids, + resources::ChildResource, roles::{RoleAssignment, RoleAssignments}, scope::Scope, }; @@ -498,6 +500,17 @@ impl PimClient { Ok(assignments) } + pub fn eligible_child_resources(&self, scope: &Scope) -> Result> { + info!("listing eligible child resources for {scope}"); + let value = self + .backend + .request(Method::GET, Operation::EligibleChildResources) + .scope(scope.clone()) + .send() + .context("unable to list eligible child resources")?; + ChildResource::parse(&value) + } + /// List all assignments (not just those managed by PIM) pub fn role_definitions(&self, scope: &Scope) -> Result> { info!("listing role definitions for {scope}"); diff --git a/src/resources.rs b/src/resources.rs new file mode 100644 index 0000000..a8d518f --- /dev/null +++ b/src/resources.rs @@ -0,0 +1,47 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeSet; + +use crate::scope::Scope; + +#[derive(Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq, Debug)] +pub struct ChildResource { + pub id: Scope, + pub name: String, + #[serde(rename = "type")] + pub type_: String, +} + +impl ChildResource { + pub(crate) fn parse(data: &Value) -> Result> { + let mut results = BTreeSet::new(); + + if let Some(value) = data.get("value") { + if let Some(value) = value.as_array() { + for entry in value { + let child_resource: ChildResource = serde_json::from_value(entry.clone())?; + results.insert(child_resource); + } + } + } + + Ok(results) + } +} + +#[cfg(test)] +mod tests { + use super::ChildResource; + use anyhow::Result; + use insta::assert_json_snapshot; + use serde_json::{from_str, Value}; + + #[test] + fn test_child_resource_parse() -> Result<()> { + let data: Value = from_str(include_str!("../tests/data/child-resources.json"))?; + let result = ChildResource::parse(&data)?; + assert_json_snapshot!(result); + Ok(()) + } +} diff --git a/src/snapshots/azure_pim_cli__resources__tests__child_resource_parse.snap b/src/snapshots/azure_pim_cli__resources__tests__child_resource_parse.snap new file mode 100644 index 0000000..087c307 --- /dev/null +++ b/src/snapshots/azure_pim_cli__resources__tests__child_resource_parse.snap @@ -0,0 +1,16 @@ +--- +source: src/resources.rs +expression: result +--- +[ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group", + "name": "DefaultResourceGroup-EUS", + "type": "resourcegroup" + }, + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/my-resource-group2", + "name": "DefaultResourceGroup-EUS", + "type": "resourcegroup" + } +] diff --git a/tests/data/child-resources.json b/tests/data/child-resources.json new file mode 100644 index 0000000..52c4150 --- /dev/null +++ b/tests/data/child-resources.json @@ -0,0 +1,14 @@ +{ + "value": [ + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group", + "name": "DefaultResourceGroup-EUS", + "type": "resourcegroup" + }, + { + "id": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/my-resource-group2", + "name": "DefaultResourceGroup-EUS", + "type": "resourcegroup" + } + ] +}