Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for Entra roles #61

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 76 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/netr0m/az-pim-cli.svg)](https://pkg.go.dev/github.com/netr0m/az-pim-cli)

`az-pim-cli` eases the process of listing and activating Azure PIM roles by allowing activation via the command line. Authentication is handled with the `azure.identity` library by utilizing the `AzureCLICredential` method.
It currently supports ['azure resources'](#azure-resources) and ['groups'](#groups).
It currently supports ['azure resources'](#azure-resources), ['groups'](#groups), and ['entra roles'](#entra-roles)

## Install
### Install with `go install`
Expand Down Expand Up @@ -102,6 +102,29 @@ Global Flags:

```

#### Entra roles
> List [entra roles](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadmigratedroles)
> :warning: Requires an access token with the appropriate scope. See [Token for Entra ID Groups and Roles](#token-for-entra-id-groups-and-roles) for more details.

```bash
$ az-pim-cli list roles --help
Query Azure PIM for eligible Entra role assignments

Usage:
az-pim-cli list role [flags]

Aliases:
role, rl, role, roles

Flags:
-h, --help help for role
-t, --token string An access token for the PIM 'Entra Roles' and 'Groups' API (required). Consult the README for more information.

Global Flags:
-c, --config string config file (default is $HOME/.az-pim-cli.yaml)

```

### Activate a role

#### Azure resources
Expand Down Expand Up @@ -164,6 +187,38 @@ Global Flags:

```

#### Entra roles
> Activate [entra roles](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadmigratedroles)
> :warning: Requires an access token with the appropriate scope. See [Token for Entra ID Groups and Roles](#token-for-entra-id-groups-and-roles) for more details.

```bash
$ az-pim-cli activate role --help
go run main.go activate role --help
Sends a request to Azure PIM to activate the given Entra role

Usage:
az-pim-cli activate role [flags]

Aliases:
role, rl, role, roles

Flags:
-h, --help help for role
-t, --token string An access token for the PIM 'Entra Roles' and 'Groups' API (required). Consult the README for more information.

Global Flags:
-c, --config string config file (default is $HOME/.az-pim-cli.yaml)
--dry-run Display the resource that would be activated, without requesting the activation
-d, --duration int Duration in minutes that the role should be activated for (default 480)
-n, --name string The name of the resource to activate
-p, --prefix string The name prefix of the resource to activate (e.g. 'S399'). Alternative to 'name'.
--reason string Reason for the activation (default "config")
-r, --role string Specify the role to activate, if multiple roles are found for a resource (e.g. 'Owner' and 'Contributor')
-T, --ticket-number string Ticket number for the activation
--ticket-system string Ticket system for the activation

```

### Examples
#### Azure resources
```bash
Expand All @@ -185,7 +240,7 @@ $ az-pim-cli activate resource --prefix s100 --role owner
2024/05/31 15:06:25 Activating role 'Owner' for resource 'S100-Example-Subscription' with reason 'config' (ticket: [])
2024/05/31 15:06:34 The role 'Owner' in 'S100-Example-Subscription' is now Provisioned

# Activate a role and specify a ticket number for the activation
# Activate a resource role and specify a ticket number for the activation
$ az-pim-cli activate resource --name S100-Example-Subscription --role Owner --ticket-system Jira --ticket-number T-1337
2024/05/31 15:06:25 Activating role 'Owner' for resource 'S100-Example-Subscription' with reason 'config' (ticket: T-1337 [Jira])
2024/05/31 15:06:34 The role 'Owner' in 'S100-Example-Subscription' is now Provisioned
Expand All @@ -204,9 +259,22 @@ $ az-pim-cli activate group --name my-entra-id-group --duration 5
2024/05/31 15:00:23 The role 'Owner' for group 'my-entra-id-group' is now Active
```

#### Entra roles
```bash
# List eligible Entra role assignments
$ az-pim-cli list roles
== my-entra-id-role ==
- Owner

# Activate the first matching role for the Entra role 'my-entra-id-role'
$ az-pim-cli activate role --name my-entra-id-role --duration 5
2024/05/31 15:00:10 Activating role 'Owner' for Entra role 'my-entra-id-role' with reason 'config' (ticket: [])
2024/05/31 15:00:23 The role 'Owner' for Entra role 'my-entra-id-role' is now Active
```

### Configuration options

- `token`: The Bearer token to use for authorization when requesting the Azure PIM Groups endpoint, i.e. listing/activating Azure PIM Groups
- `token`: The Bearer token to use for authorization when requesting the Azure PIM Groups endpoint, i.e. listing/activating Azure PIM Groups and Entra Roles

#### YAML file
You may define configuration options in a YAML file.
Expand All @@ -229,13 +297,13 @@ export PIM_TOKEN=eyJ0[...]

```

### Token for Entra ID Groups
### Token for Entra ID Groups and Roles
Due to limitations with authorization for Azure PIM, this software may only acquire a token authorized for listing and activating ['Azure resources' roles](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/azurerbac).
In order to list or activate ['Entra groups'](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadgroup), you must acquire a token from an authenticated browser session. This token will have a limited lifetime, which means you'll likely have to perform this step each time you wish to activate or list Entra groups.
In order to list or activate ['Entra groups'](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadgroup) and ['Entra roles'](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadmigratedroles), you must acquire a token from an authenticated browser session. This token will have a limited lifetime, which means you'll likely have to perform this step each time you wish to activate or list Entra groups.

To acquire the token, do the following:
1. Navigate to ['Microsoft Entra Privileged Identity Management > Activate > Groups'](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadgroup)
2. Open *DevTools* (`CTRL+Shift+I`), and locate a request to `https://api.azrbac.mspim.azure.com/api/v2/privilegedAccess/aadGroups/roleAssignments`
1. Navigate to ['Microsoft Entra Privileged Identity Management > Activate > Groups'](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadgroup) or ['Microsoft Entra Privileged Identity Management > Activate > Microsoft Entra roles'](https://portal.azure.com/#view/Microsoft_Azure_PIMCommon/ActivationMenuBlade/~/aadmigratedroles)
2. Open *DevTools* (`CTRL+Shift+I`), and locate a request to `https://api.azrbac.mspim.azure.com/api/v2/privilegedAccess/aadGroups/roleAssignments` or `https://api.azrbac.mspim.azure.com/api/v2/privilegedAccess/aadroles/roleAssignments`
- If no such request can be seen, press the "Refresh" button above the table to issue a new request
- In *DevTools*, the "File" attribute should start with "roleAssignments"
3. In *DevTools*, under the "Headers" tab for the given request, copy the value of the `Authorization` header, which should start with "Bearer eyJ0[...]"
Expand All @@ -244,7 +312,7 @@ To acquire the token, do the following:
```
PIM_TOKEN=eyJ0[...]
```
6. You may now, and for the duration of the token's lifetime, list and activate 'Entra groups' using this tool
6. You may now, and for the duration of the token's lifetime, list and activate 'Entra groups' and 'Entra roles' using this tool

## Contributing

Expand Down
42 changes: 37 additions & 5 deletions cmd/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ var activateGroupCmd = &cobra.Command{
Aliases: []string{"g", "grp", "groups"},
Short: "Sends a request to Azure PIM to activate the given group",
Run: func(cmd *cobra.Command, args []string) {
subjectId := pim.GetUserInfo(pimGroupsToken).ObjectId
subjectId := pim.GetUserInfo(pimGovernanceRoleToken).ObjectId

eligibleGroupAssignments := pim.GetEligibleGroupAssignments(pimGroupsToken, subjectId)
groupAssignment := utils.GetGroupAssignment(name, prefix, roleName, eligibleGroupAssignments)
eligibleGroupAssignments := pim.GetEligibleGovernanceRoleAssignments(pim.ROLE_TYPE_AAD_GROUPS, subjectId, pimGovernanceRoleToken)
groupAssignment := utils.GetGovernanceRoleAssignment(name, prefix, roleName, eligibleGroupAssignments)

log.Printf(
"Activating role '%s' for group '%s' with reason '%s' (ticket: %s [%s])",
Expand All @@ -80,15 +80,44 @@ var activateGroupCmd = &cobra.Command{
log.Printf("Skipping activation due to 'dry-run'.")
os.Exit(0)
}
requestResponse := pim.RequestGroupAssignment(subjectId, groupAssignment, duration, reason, ticketSystem, ticketNumber, pimGroupsToken)
requestResponse := pim.RequestGovernanceRoleAssignment(subjectId, pim.ROLE_TYPE_AAD_GROUPS, groupAssignment, duration, reason, ticketSystem, ticketNumber, pimGovernanceRoleToken)
log.Printf("The role '%s' for group '%s' is now %s", groupAssignment.RoleDefinition.DisplayName, groupAssignment.RoleDefinition.Resource.DisplayName, requestResponse.AssignmentState)
},
}

var activateEntraRoleCmd = &cobra.Command{
Use: "role",
Aliases: []string{"rl", "role", "roles"},
Short: "Sends a request to Azure PIM to activate the given Entra role",
Run: func(cmd *cobra.Command, args []string) {
subjectId := pim.GetUserInfo(pimGovernanceRoleToken).ObjectId

eligibleEntraRoleAssignments := pim.GetEligibleGovernanceRoleAssignments(pim.ROLE_TYPE_ENTRA_ROLES, subjectId, pimGovernanceRoleToken)
entraRoleAssignment := utils.GetGovernanceRoleAssignment(name, prefix, roleName, eligibleEntraRoleAssignments)

log.Printf(
"Activating role '%s' for Entra role '%s' with reason '%s' (ticket: %s [%s])",
entraRoleAssignment.RoleDefinition.DisplayName,
entraRoleAssignment.RoleDefinition.Resource.DisplayName,
reason,
ticketNumber,
ticketSystem,
)

if dryRun {
log.Printf("Skipping activation due to 'dry-run'.")
os.Exit(0)
}
requestResponse := pim.RequestGovernanceRoleAssignment(subjectId, pim.ROLE_TYPE_AAD_GROUPS, entraRoleAssignment, duration, reason, ticketSystem, ticketNumber, pimGovernanceRoleToken)
log.Printf("The role '%s' for Entra role '%s' is now %s", entraRoleAssignment.RoleDefinition.DisplayName, entraRoleAssignment.RoleDefinition.Resource.DisplayName, requestResponse.AssignmentState)
},
}

func init() {
rootCmd.AddCommand(activateCmd)
activateCmd.AddCommand(activateResourceCmd)
activateCmd.AddCommand(activateGroupCmd)
activateCmd.AddCommand(activateEntraRoleCmd)

// Flags
activateCmd.PersistentFlags().StringVarP(&name, "name", "n", "", "The name of the resource to activate")
Expand All @@ -100,9 +129,12 @@ func init() {
activateCmd.PersistentFlags().StringVarP(&ticketNumber, "ticket-number", "T", "", "Ticket number for the activation")
activateCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Display the resource that would be activated, without requesting the activation")

activateGroupCmd.PersistentFlags().StringVarP(&pimGroupsToken, "token", "t", "", "An access token for the PIM 'Entra Roles' and 'Groups' API (required). Consult the README for more information.")
activateGroupCmd.PersistentFlags().StringVarP(&pimGovernanceRoleToken, "token", "t", "", "An access token for the PIM 'Entra Roles' and 'Groups' API (required). Consult the README for more information.")
activateGroupCmd.MarkPersistentFlagRequired("token") //nolint:errcheck

activateEntraRoleCmd.PersistentFlags().StringVarP(&pimGovernanceRoleToken, "token", "t", "", "An access token for the PIM 'Entra Roles' and 'Groups' API (required). Consult the README for more information.")
activateEntraRoleCmd.MarkPersistentFlagRequired("token") //nolint:errcheck

activateCmd.MarkFlagsOneRequired("name", "prefix")
activateCmd.MarkFlagsMutuallyExclusive("name", "prefix")
}
22 changes: 18 additions & 4 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,31 @@ var listGroupCmd = &cobra.Command{
Aliases: []string{"g", "grp", "groups"},
Short: "Query Azure PIM for eligible group assignments",
Run: func(cmd *cobra.Command, args []string) {
subjectId := pim.GetUserInfo(pimGroupsToken).ObjectId
eligibleGroupAssignments := pim.GetEligibleGroupAssignments(pimGroupsToken, subjectId)
utils.PrintEligibleGroups(eligibleGroupAssignments)
subjectId := pim.GetUserInfo(pimGovernanceRoleToken).ObjectId
eligibleGroupAssignments := pim.GetEligibleGovernanceRoleAssignments(pim.ROLE_TYPE_AAD_GROUPS, subjectId, pimGovernanceRoleToken)
utils.PrintEligibleGovernanceRoles(eligibleGroupAssignments)
},
}

var listEntraRoleCmd = &cobra.Command{
Use: "role",
Aliases: []string{"rl", "role", "roles"},
Short: "Query Azure PIM for eligible Entra role assignments",
Run: func(cmd *cobra.Command, args []string) {
subjectId := pim.GetUserInfo(pimGovernanceRoleToken).ObjectId
eligibleEntraRoleAssignments := pim.GetEligibleGovernanceRoleAssignments("aadroles", subjectId, pimGovernanceRoleToken)
utils.PrintEligibleGovernanceRoles(eligibleEntraRoleAssignments)
},
}

func init() {
rootCmd.AddCommand(listCmd)
listCmd.AddCommand(listResourceCmd)
listCmd.AddCommand(listGroupCmd)
listCmd.AddCommand(listEntraRoleCmd)

listGroupCmd.PersistentFlags().StringVarP(&pimGroupsToken, "token", "t", "", "An access token for the PIM Groups API (required). Consult the README for more information.")
listGroupCmd.PersistentFlags().StringVarP(&pimGovernanceRoleToken, "token", "t", "", "An access token for the PIM 'Entra Roles' and 'Groups' API (required). Consult the README for more information.")
listGroupCmd.MarkPersistentFlagRequired("token") //nolint:errcheck
listEntraRoleCmd.PersistentFlags().StringVarP(&pimGovernanceRoleToken, "token", "t", "", "An access token for the PIM 'Entra Roles' and 'Groups' API (required). Consult the README for more information.")
listEntraRoleCmd.MarkPersistentFlagRequired("token") //nolint:errcheck
}
4 changes: 3 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

var cfgFile string
var pimGroupsToken string
var pimGovernanceRoleToken string

var rootCmd = &cobra.Command{
Use: "az-pim-cli",
Expand Down Expand Up @@ -72,7 +72,9 @@ func initConfig() {
bindFlags(rootCmd, vpr)
bindFlags(activateCmd, vpr)
bindFlags(listGroupCmd, vpr)
bindFlags(listEntraRoleCmd, vpr)
bindFlags(activateGroupCmd, vpr)
bindFlags(activateEntraRoleCmd, vpr)
}

func bindFlags(cmd *cobra.Command, vpr *viper.Viper) {
Expand Down
Loading