diff --git a/README.md b/README.md index 19ef1df..6bd5451 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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 @@ -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 @@ -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 @@ -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. @@ -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[...]" @@ -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 diff --git a/cmd/activate.go b/cmd/activate.go index 7817196..c2486e1 100644 --- a/cmd/activate.go +++ b/cmd/activate.go @@ -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])", @@ -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") @@ -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") } diff --git a/cmd/list.go b/cmd/list.go index 4037c95..a07a8a8 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -33,9 +33,20 @@ 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) }, } @@ -43,7 +54,10 @@ 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 } diff --git a/cmd/root.go b/cmd/root.go index bb3b8ac..69a7105 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,7 @@ import ( ) var cfgFile string -var pimGroupsToken string +var pimGovernanceRoleToken string var rootCmd = &cobra.Command{ Use: "az-pim-cli", @@ -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) { diff --git a/pkg/pim/client.go b/pkg/pim/client.go index 7b6195b..545b897 100644 --- a/pkg/pim/client.go +++ b/pkg/pim/client.go @@ -121,14 +121,17 @@ func GetEligibleResourceAssignments(token string) *ResourceAssignmentResponse { return responseModel } -func GetEligibleGroupAssignments(token string, subjectId string) *GroupAssignmentResponse { +func GetEligibleGovernanceRoleAssignments(roleType string, subjectId string, token string) *GovernanceRoleAssignmentResponse { + if !IsGovernanceRoleType(roleType) { + log.Fatalln("GetEligibleGovernanceRoleAssignments: Invalid role type specified.") + } var params = map[string]string{ "$expand": "linkedEligibleRoleAssignment,subject,scopedResource,roleDefinition($expand=resource)", "$filter": fmt.Sprintf("(subject/id eq '%s') and (assignmentState eq 'Eligible')", subjectId), } - responseModel := &GroupAssignmentResponse{} + responseModel := &GovernanceRoleAssignmentResponse{} _ = Request(&PIMRequest{ - Url: fmt.Sprintf("%s/%s/aadGroups/roleAssignments", AZ_PIM_GROUP_BASE_URL, AZ_PIM_GROUP_BASE_PATH), + Url: fmt.Sprintf("%s/%s/%s/roleAssignments", AZ_PIM_GOV_ROLE_BASE_URL, AZ_PIM_GOV_ROLE_BASE_PATH, roleType), Token: token, Method: "GET", Params: params, @@ -179,35 +182,38 @@ func ValidateResourceAssignmentRequest(scope string, resourceAssignmentRequest R return false } -func ValidateGroupAssignmentRequest(groupAssignmentRequest GroupAssignmentRequest, token string) bool { +func ValidateGovernanceRoleAssignmentRequest(roleType string, roleAssignmentRequest GovernanceRoleAssignmentRequest, token string) bool { + if !IsGovernanceRoleType(roleType) { + log.Fatalln("ValidateGovernanceRoleAssignmentRequest: Invalid role type specified.") + } var params = map[string]string{ "evaluateOnly": "true", } - groupAssignmentValidationRequest := groupAssignmentRequest - groupAssignmentValidationRequest.Reason = "Evaluate Only" - groupAssignmentValidationRequest.TicketNumber = "Evaluate Only" - groupAssignmentValidationRequest.TicketSystem = "Evaluate Only" + governanceRoleAssignmentValidationRequest := roleAssignmentRequest + governanceRoleAssignmentValidationRequest.Reason = "Evaluate Only" + governanceRoleAssignmentValidationRequest.TicketNumber = "Evaluate Only" + governanceRoleAssignmentValidationRequest.TicketSystem = "Evaluate Only" - validationResponse := &GroupAssignmentRequestResponse{} + validationResponse := &GovernanceRoleAssignmentRequestResponse{} _ = Request(&PIMRequest{ - Url: fmt.Sprintf("%s/%s/aadGroups/roleAssignmentRequests", AZ_PIM_GROUP_BASE_URL, AZ_PIM_GROUP_BASE_PATH), + Url: fmt.Sprintf("%s/%s/%s/roleAssignmentRequests", AZ_PIM_GOV_ROLE_BASE_URL, AZ_PIM_GOV_ROLE_BASE_PATH, roleType), Token: token, Method: "POST", Params: params, - Payload: groupAssignmentValidationRequest, + Payload: governanceRoleAssignmentValidationRequest, }, validationResponse) - if IsGroupAssignmentRequestFailed(validationResponse) { - log.Printf("ERROR: The group assignment validation failed with status '%s', '%s'", validationResponse.Status.Status, validationResponse.Status.SubStatus) + if IsGovernanceRoleAssignmentRequestFailed(validationResponse) { + log.Printf("ERROR: The role assignment validation failed with status '%s', '%s'", validationResponse.Status.Status, validationResponse.Status.SubStatus) log.Fatalln(validationResponse) return false } - if IsGroupAssignmentRequestOK(validationResponse) { + if IsGovernanceRoleAssignmentRequestOK(validationResponse) { return true } - if IsGroupAssignmentRequestPending(validationResponse) { - log.Printf("WARNING: The group assignment request is pending with status '%s', '%s'", validationResponse.Status.Status, validationResponse.Status.SubStatus) + if IsGovernanceRoleAssignmentRequestPending(validationResponse) { + log.Printf("WARNING: The role assignment request is pending with status '%s', '%s'", validationResponse.Status.Status, validationResponse.Status.SubStatus) return true } @@ -260,34 +266,37 @@ func RequestResourceAssignment(subjectId string, resourceAssignment *ResourceAss return responseModel } -func RequestGroupAssignment(subjectId string, groupAssignment *GroupAssignment, duration int, reason string, ticketSystem string, ticketNumber string, token string) *GroupAssignmentRequestResponse { - groupAssignmentRequest := &GroupAssignmentRequest{ - RoleDefinitionId: groupAssignment.RoleDefinitionId, - ResourceId: groupAssignment.ResourceId, +func RequestGovernanceRoleAssignment(subjectId string, roleType string, governanceRoleAssignment *GovernanceRoleAssignment, duration int, reason string, ticketSystem string, ticketNumber string, token string) *GovernanceRoleAssignmentRequestResponse { + if !IsGovernanceRoleType(roleType) { + log.Fatalln("RequestGovernanceRoleAssignment: Invalid role type specified.") + } + governanceRoleAssignmentRequest := &GovernanceRoleAssignmentRequest{ + RoleDefinitionId: governanceRoleAssignment.RoleDefinitionId, + ResourceId: governanceRoleAssignment.ResourceId, SubjectId: subjectId, AssignmentState: "Active", Type: "UserAdd", Reason: reason, TicketNumber: ticketNumber, TicketSystem: ticketSystem, - Schedule: &GroupAssignmentSchedule{ + Schedule: &GovernanceRoleAssignmentSchedule{ Type: "Once", StartDateTime: nil, EndDateTime: nil, Duration: fmt.Sprintf("PT%dM", duration), }, - LinkedEligibleRoleAssignmentId: groupAssignment.Id, + LinkedEligibleRoleAssignmentId: governanceRoleAssignment.Id, ScopedResourceId: "", } - ValidateGroupAssignmentRequest(*groupAssignmentRequest, token) + ValidateGovernanceRoleAssignmentRequest(roleType, *governanceRoleAssignmentRequest, token) - responseModel := &GroupAssignmentRequestResponse{} + responseModel := &GovernanceRoleAssignmentRequestResponse{} _ = Request(&PIMRequest{ - Url: fmt.Sprintf("%s/%s/aadGroups/roleAssignmentRequests", AZ_PIM_GROUP_BASE_URL, AZ_PIM_GROUP_BASE_PATH), + Url: fmt.Sprintf("%s/%s/%s/roleAssignmentRequests", AZ_PIM_GOV_ROLE_BASE_URL, AZ_PIM_GOV_ROLE_BASE_PATH, roleType), Token: token, Method: "POST", - Payload: groupAssignmentRequest, + Payload: governanceRoleAssignmentRequest, }, responseModel) return responseModel diff --git a/pkg/pim/const.go b/pkg/pim/const.go index 76d00b7..6ef2818 100644 --- a/pkg/pim/const.go +++ b/pkg/pim/const.go @@ -6,14 +6,14 @@ package pim // Base URL for the Azure Entra PIM API const AZ_PIM_BASE_URL string = "https://management.azure.com" -// Base URL for the Azure PIM Groups API -const AZ_PIM_GROUP_BASE_URL string = "https://api.azrbac.mspim.azure.com" +// Base URL for the Azure PIM Governance Role API +const AZ_PIM_GOV_ROLE_BASE_URL string = "https://api.azrbac.mspim.azure.com" // Base path for the Azure Entra PIM API const AZ_PIM_BASE_PATH string = "providers/Microsoft.Authorization" -// Base path for the Azure PIM Groups API -const AZ_PIM_GROUP_BASE_PATH = "api/v2/privilegedAccess" +// Base path for the Azure PIM Governance Role API +const AZ_PIM_GOV_ROLE_BASE_PATH = "api/v2/privilegedAccess" // Authority used for Azure authentication const AZ_AUTHORITY string = "https://login.microsoftonline.com/" @@ -29,3 +29,9 @@ const DEFAULT_DURATION_MINUTES int = 480 // API version for the "role eligibility schedule instances" (i.e. eligible azure resource role assignments) const AZ_PIM_API_VERSION string = "2020-10-01" + +// Role types +const ( + ROLE_TYPE_AAD_GROUPS = "aadGroups" + ROLE_TYPE_ENTRA_ROLES = "aadroles" +) diff --git a/pkg/pim/models.go b/pkg/pim/models.go index 23284a5..f20c734 100644 --- a/pkg/pim/models.go +++ b/pkg/pim/models.go @@ -64,7 +64,7 @@ type ResourceAssignmentResponse struct { Value []ResourceAssignment `json:"value"` } -type GroupAssignmentSubject struct { +type GovernanceRoleAssignmentSubject struct { Id string `json:"id"` Type string `json:"type"` DisplayName string `json:"displayName"` @@ -72,34 +72,34 @@ type GroupAssignmentSubject struct { Email string `json:"email"` } -type GroupResource struct { +type GovernanceRoleResource struct { Id string `json:"id"` Type string `json:"type"` DisplayName string `json:"displayName"` Status string `json:"status"` } -type GroupDefinition struct { - Id string `json:"id"` - ResourceId string `json:"resourceId"` - Type string `json:"type"` - DisplayName string `json:"displayName"` - Resource *GroupResource `json:"resource"` +type GovernanceRoleDefinition struct { + Id string `json:"id"` + ResourceId string `json:"resourceId"` + Type string `json:"type"` + DisplayName string `json:"displayName"` + Resource *GovernanceRoleResource `json:"resource"` } -type GroupAssignment struct { - Id string `json:"id"` - ResourceId string `json:"resourceId"` - RoleDefinitionId string `json:"roleDefinitionId"` - SubjectId string `json:"subjectId"` - AssignmentState string `json:"assignmentState"` - Status string `json:"status"` - Subject *GroupAssignmentSubject `json:"subject"` - RoleDefinition *GroupDefinition `json:"roleDefinition"` +type GovernanceRoleAssignment struct { + Id string `json:"id"` + ResourceId string `json:"resourceId"` + RoleDefinitionId string `json:"roleDefinitionId"` + SubjectId string `json:"subjectId"` + AssignmentState string `json:"assignmentState"` + Status string `json:"status"` + Subject *GovernanceRoleAssignmentSubject `json:"subject"` + RoleDefinition *GovernanceRoleDefinition `json:"roleDefinition"` } -type GroupAssignmentResponse struct { - Value []GroupAssignment `json:"value"` +type GovernanceRoleAssignmentResponse struct { + Value []GovernanceRoleAssignment `json:"value"` } type TicketInfo struct { @@ -183,52 +183,52 @@ type ResourceAssignmentRequestRequest struct { Properties ResourceAssignmentRequestProperties `json:"Properties"` } -type GroupAssignmentSchedule struct { +type GovernanceRoleAssignmentSchedule struct { Type string `json:"type"` StartDateTime interface{} `json:"startDateTime"` EndDateTime interface{} `json:"endDateTime"` Duration string `json:"duration"` } -type GroupAssignmentRequest struct { - RoleDefinitionId string `json:"roleDefinitionId"` - ResourceId string `json:"resourceId"` - SubjectId string `json:"subjectId"` - AssignmentState string `json:"assignmentState"` - Type string `json:"type"` - Reason string `json:"reason"` - TicketNumber string `json:"ticketNumber"` - TicketSystem string `json:"ticketSystem"` - Schedule *GroupAssignmentSchedule `json:"schedule"` - LinkedEligibleRoleAssignmentId string `json:"linkedEligibleRoleAssignmentId"` - ScopedResourceId string `json:"scopedResourceId"` +type GovernanceRoleAssignmentRequest struct { + RoleDefinitionId string `json:"roleDefinitionId"` + ResourceId string `json:"resourceId"` + SubjectId string `json:"subjectId"` + AssignmentState string `json:"assignmentState"` + Type string `json:"type"` + Reason string `json:"reason"` + TicketNumber string `json:"ticketNumber"` + TicketSystem string `json:"ticketSystem"` + Schedule *GovernanceRoleAssignmentSchedule `json:"schedule"` + LinkedEligibleRoleAssignmentId string `json:"linkedEligibleRoleAssignmentId"` + ScopedResourceId string `json:"scopedResourceId"` } -type GroupAssignmentRequestStatus struct { +type GovernanceRoleAssignmentRequestStatus struct { Status string `json:"status"` SubStatus string `json:"subStatus"` StatusDetails []map[string]string `json:"statusDetails"` } -type GroupAssignmentRequestResponse struct { - Id string `json:"id"` - ResourceId string `json:"resourceId"` - RoleDefinitionId string `json:"roleDefinitionId"` - SubjectId string `json:"subjectId"` - ScopedResourceId string `json:"scopedResourceId"` - LinkedEligibleRoleAssignmentId string `json:"linkedEligibleRoleAssignmentId"` - Type string `json:"type"` - AssignmentState string `json:"assignmentState"` - RequestedDateTime string `json:"requestedDateTime"` - RoleAssignmentStartDateTime string `json:"roleAssignmentStartDateTime"` - RoleAssignmentEndDateTime string `json:"roleAssignmentEndDateTime"` - Reason string `json:"reason"` - TicketNumber string `json:"ticketNumber"` - TicketSystem string `json:"ticketSystem"` - Condition string `json:"condition"` - ConditionVersion string `json:"conditionVersion"` - ConditionDescription string `json:"conditionDescription"` - Status *GroupAssignmentRequestStatus `json:"status"` - Schedule *GroupAssignmentSchedule `json:"schedule"` - Metadata map[string]interface{} `json:"metadata"` +type GovernanceRoleAssignmentRequestResponse struct { + Id string `json:"id"` + ResourceId string `json:"resourceId"` + RoleDefinitionId string `json:"roleDefinitionId"` + SubjectId string `json:"subjectId"` + ScopedResourceId string `json:"scopedResourceId"` + LinkedEligibleRoleAssignmentId string `json:"linkedEligibleRoleAssignmentId"` + Type string `json:"type"` + AssignmentState string `json:"assignmentState"` + RequestedDateTime string `json:"requestedDateTime"` + RoleAssignmentStartDateTime string `json:"roleAssignmentStartDateTime"` + RoleAssignmentEndDateTime string `json:"roleAssignmentEndDateTime"` + Reason string `json:"reason"` + TicketNumber string `json:"ticketNumber"` + TicketSystem string `json:"ticketSystem"` + Condition string `json:"condition"` + ConditionVersion string `json:"conditionVersion"` + ConditionDescription string `json:"conditionDescription"` + Status *GovernanceRoleAssignmentRequestStatus `json:"status"` + Schedule *GovernanceRoleAssignmentSchedule `json:"schedule"` + Metadata map[string]interface{} `json:"metadata"` } diff --git a/pkg/pim/utils.go b/pkg/pim/utils.go index 904abd7..f4119e7 100644 --- a/pkg/pim/utils.go +++ b/pkg/pim/utils.go @@ -11,7 +11,7 @@ func IsResourceAssignmentRequestFailed(requestResponse *ResourceAssignmentReques return false } -func IsGroupAssignmentRequestFailed(requestResponse *GroupAssignmentRequestResponse) bool { +func IsGovernanceRoleAssignmentRequestFailed(requestResponse *GovernanceRoleAssignmentRequestResponse) bool { switch requestResponse.Status.SubStatus { case StatusAdminDenied, StatusCanceled, StatusDenied, StatusFailed, StatusFailedAsResourceIsLocked, StatusInvalid, StatusRevoked, StatusTimedOut: return true @@ -27,7 +27,7 @@ func IsResourceAssignmentRequestPending(requestResponse *ResourceAssignmentReque return false } -func IsGroupAssignmentRequestPending(requestResponse *GroupAssignmentRequestResponse) bool { +func IsGovernanceRoleAssignmentRequestPending(requestResponse *GovernanceRoleAssignmentRequestResponse) bool { switch requestResponse.Status.SubStatus { case StatusPendingAdminDecision, StatusPendingApproval, StatusPendingApprovalProvisioning, StatusPendingEvaluation, StatusPendingExternalProvisioning, StatusPendingProvisioning, StatusPendingRevocation, StatusPendingScheduleCreation: return true @@ -43,10 +43,18 @@ func IsResourceAssignmentRequestOK(requestResponse *ResourceAssignmentRequestRes return false } -func IsGroupAssignmentRequestOK(requestResponse *GroupAssignmentRequestResponse) bool { +func IsGovernanceRoleAssignmentRequestOK(requestResponse *GovernanceRoleAssignmentRequestResponse) bool { switch requestResponse.Status.SubStatus { case StatusAccepted, StatusAdminApproved, StatusGranted, StatusProvisioned, StatusProvisioningStarted, StatusScheduleCreated: return true } return false } + +func IsGovernanceRoleType(roleType string) bool { + switch roleType { + case ROLE_TYPE_AAD_GROUPS, ROLE_TYPE_ENTRA_ROLES: + return true + } + return false +} diff --git a/pkg/utils/main.go b/pkg/utils/main.go index fb2ca75..30f81c6 100644 --- a/pkg/utils/main.go +++ b/pkg/utils/main.go @@ -31,20 +31,20 @@ func PrintEligibleResources(resourceAssignments *pim.ResourceAssignmentResponse) } } -func PrintEligibleGroups(groupAssignments *pim.GroupAssignmentResponse) { - var eligibleGroups = make(map[string][]string) - - for _, groupAssignment := range groupAssignments.Value { - groupName := groupAssignment.RoleDefinition.Resource.DisplayName - roleName := groupAssignment.RoleDefinition.DisplayName - if _, ok := eligibleGroups[groupName]; !ok { - eligibleGroups[groupName] = []string{} +func PrintEligibleGovernanceRoles(governanceRoleAssignments *pim.GovernanceRoleAssignmentResponse) { + var eligibleGovernanceRoles = make(map[string][]string) + + for _, governanceRoleAssignment := range governanceRoleAssignments.Value { + governanceRoleName := governanceRoleAssignment.RoleDefinition.Resource.DisplayName + roleName := governanceRoleAssignment.RoleDefinition.DisplayName + if _, ok := eligibleGovernanceRoles[governanceRoleName]; !ok { + eligibleGovernanceRoles[governanceRoleName] = []string{} } - eligibleGroups[groupName] = append(eligibleGroups[groupName], roleName) + eligibleGovernanceRoles[governanceRoleName] = append(eligibleGovernanceRoles[governanceRoleName], roleName) } - for grp, rol := range eligibleGroups { - fmt.Printf("== %s ==\n", grp) + for govRole, rol := range eligibleGovernanceRoles { + fmt.Printf("== %s ==\n", govRole) for role := range rol { fmt.Printf("\t - %s\n", rol[role]) } @@ -84,35 +84,35 @@ func GetResourceAssignment(name string, prefix string, role string, eligibleReso return nil } -func GetGroupAssignment(name string, prefix string, role string, eligibleGroupAssignments *pim.GroupAssignmentResponse) *pim.GroupAssignment { +func GetGovernanceRoleAssignment(name string, prefix string, role string, eligibleGovernanceRoleAssignments *pim.GovernanceRoleAssignmentResponse) *pim.GovernanceRoleAssignment { name = strings.ToLower(name) prefix = strings.ToLower(prefix) role = strings.ToLower(role) - for _, eligibleGroupAssignment := range eligibleGroupAssignments.Value { - var match *pim.GroupAssignment = nil - currentGroupName := strings.ToLower(eligibleGroupAssignment.RoleDefinition.Resource.DisplayName) + for _, eligibleGovernanceRoleAssignment := range eligibleGovernanceRoleAssignments.Value { + var match *pim.GovernanceRoleAssignment = nil + currentGovernanceRoleName := strings.ToLower(eligibleGovernanceRoleAssignment.RoleDefinition.Resource.DisplayName) if len(prefix) != 0 { - if strings.HasPrefix(currentGroupName, prefix) { - match = &eligibleGroupAssignment // #nosec G601 false positive with go >= v1.22 + if strings.HasPrefix(currentGovernanceRoleName, prefix) { + match = &eligibleGovernanceRoleAssignment // #nosec G601 false positive with go >= v1.22 } } else if len(name) != 0 { - if currentGroupName == name { - match = &eligibleGroupAssignment // #nosec G601 false positive with go >= v1.22 + if currentGovernanceRoleName == name { + match = &eligibleGovernanceRoleAssignment // #nosec G601 false positive with go >= v1.22 } } if match != nil { if role == "" { - return &eligibleGroupAssignment + return &eligibleGovernanceRoleAssignment } - if strings.ToLower(eligibleGroupAssignment.RoleDefinition.DisplayName) == role { - return &eligibleGroupAssignment + if strings.ToLower(eligibleGovernanceRoleAssignment.RoleDefinition.DisplayName) == role { + return &eligibleGovernanceRoleAssignment } } } - log.Fatalln("Unable to find a group assignment matching the parameters.") + log.Fatalln("Unable to find a role assignment matching the parameters.") return nil }