From 6fddc870a990bc6065b8dd053544fc141421428f Mon Sep 17 00:00:00 2001 From: Morten Amundsen Date: Mon, 10 Jun 2024 08:50:40 +0200 Subject: [PATCH] feat: support for PIM Entra groups (#16) --- README.md | 154 ++++++++++++++++++++++++++++++++++++++++------ cmd/activate.go | 43 ++++++++++++- cmd/list.go | 19 +++++- cmd/root.go | 2 + pkg/pim/client.go | 106 +++++++++++++++++++++++++++---- pkg/pim/const.go | 6 ++ pkg/pim/models.go | 90 ++++++++++++++++++++++++++- pkg/pim/utils.go | 24 ++++++++ pkg/utils/main.go | 41 ++++++++++++ 9 files changed, 453 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index de4a16b..1a24acd 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ $ mv ./az-pim-cli /usr/local/bin ## Configuration In addition to supporting environment variables and command line arguments, the script also supports certain config parameters stored in a file. By default, the script will try to look for a YAML config file at `$HOME/.az-pim-cli.yaml`, but you may also override the config file to use by supplying the `--config` flag. +See [Configuration options](#configuration-options) for more details ### Prerequisites This tool depends on [`az-cli`](https://learn.microsoft.com/en-us/cli/azure/) for authentication. Please ensure that you've authenticated with your Azure tenant by running the command `az login`. A new browser window will open, asking you to authenticate. This should only be necessary to do once. @@ -44,42 +45,76 @@ Available Commands: list Query Azure PIM for eligible role assignments Flags: - -c, --config string config file (default is $HOME/.az-pim-cli.yaml) - -h, --help help for az-pim-cli + -c, --config string config file (default is $HOME/.az-pim-cli.yaml) + -h, --help help for az-pim-cli + -t, --token string An access token for the PIM Groups API Use "az-pim-cli [command] --help" for more information about a command. ``` -### List eligible role assignments +### List eligible role assignments (Azure resources) ```bash $ az-pim-cli list --help Query Azure PIM for eligible role assignments Usage: az-pim-cli list [flags] + az-pim-cli list [command] Aliases: list, l, ls +Available Commands: + group Query Azure PIM for eligible group assignments + Flags: -h, --help help for list Global Flags: - -c, --config string config file (default is $HOME/.az-pim-cli.yaml) + -c, --config string config file (default is $HOME/.az-pim-cli.yaml) + -t, --token string An access token for the PIM Groups API + +Use "az-pim-cli list [command] --help" for more information about a command. + ``` -### Activate a role +### List eligible group assignments (Entra Groups) +> :warn: Requires an access token with the appropriate scope. See [Token for Entra ID Groups](#token-for-entra-id-groups) for more details. +```bash +$ az-pim-cli list group --help +Query Azure PIM for eligible group assignments + +Usage: + az-pim-cli list group [flags] + +Aliases: + group, g, grp, groups + +Flags: + -h, --help help for group + +Global Flags: + -c, --config string config file (default is $HOME/.az-pim-cli.yaml) + -t, --token string An access token for the PIM Groups API + +``` + +### Activate a role (Azure resources) ```bash $ az-pim-cli activate --help Sends a request to Azure PIM to activate the given role Usage: az-pim-cli activate [flags] + az-pim-cli activate [command] Aliases: activate, a, ac, act +Available Commands: + group Sends a request to Azure PIM to activate the given group + Flags: -d, --duration int Duration in minutes that the role should be activated for (default 480) -h, --help help for activate @@ -89,26 +124,109 @@ Flags: -p, --subscription-prefix string The name prefix of the subscription to activate (e.g. 'S399'). Alternative to 'subscription-name'. Global Flags: - -c, --config string config file (default is $HOME/.az-pim-cli.yaml) + -c, --config string config file (default is $HOME/.az-pim-cli.yaml) + -t, --token string An access token for the PIM Groups API + +Use "az-pim-cli activate [command] --help" for more information about a command. + +``` + +### Activate a role (Entra Groups) +> :warn: Requires an access token with the appropriate scope. See [Token for Entra ID Groups](#token-for-entra-id-groups) for more details. +```bash +$ az-pim-cli activate group --help +Sends a request to Azure PIM to activate the given group + +Usage: + az-pim-cli activate group [group name] [flags] + +Aliases: + group, g, grp, groups + +Flags: + -h, --help help for group + +Global Flags: + -c, --config string config file (default is $HOME/.az-pim-cli.yaml) + -d, --duration int Duration in minutes that the role should be activated for (default 480) + --reason string Reason for the activation (default "config") + -r, --role-name string Specify the role to activate, if multiple roles are found for a subscription (e.g. 'Owner' and 'Contributor') + -s, --subscription-name string The name of the subscription to activate + -p, --subscription-prefix string The name prefix of the subscription to activate (e.g. 'S399'). Alternative to 'subscription-name'. + -t, --token string An access token for the PIM Groups API + ``` ### Examples +#### Azure resources ```bash -# List eligible role assignments +# List eligible Azure resource role assignments $ az-pim-cli list -Opening in existing browser session. -== S398-XXX == - - Owner +== S100-Example-Subscription == - Contributor -== S250-XXX == + - Owner +== S1337-Another-Subscription == - Contributor -# Activate the first matching role in a subscription with the prefix 's398' -$ az-pim-cli activate --subscription-prefix s398 --duration 60 -Opening in existing browser session. -2023/06/30 14:27:04 Activating role 'Owner' in subscription 'S398-XXX' -2023/06/30 14:27:11 The role 'Owner' in 'S398-XXX' is now Active +# Activate the first matching role in a subscription with the prefix 'S100' +$ az-pim-cli activate --subscription-prefix S100 +2024/05/31 15:05:25 Activating role 'Contributor' in subscription 'S100-Example-Subscription' with reason 'config' +2024/05/31 15:05:34 The role 'Contributor' in 'S100-Example-Subscription' is now Provisioned + +# Activate a specific role ('Owner') in a subscription with the prefix 's100' +$ az-pim-cli activate -p s100 --role-name owner +2024/05/31 15:06:25 Activating role 'Owner' in subscription 'S100-Example-Subscription' with reason 'config' +2024/05/31 15:06:34 The role 'Owner' in 'S100-Example-Subscription' is now Provisioned +``` + +#### Entra groups +```bash +# List eligible group assignments +$ az-pim-cli list groups +== my-entra-id-group == + - Owner + +# Activate the first matching role for the group 'my-entra-id-group' +$ az-pim-cli activate group my-entra-id-group -d 5 +2024/05/31 15:00:10 Activating role 'Owner' for group 'my-entra-id-group' with reason 'config' +2024/05/31 15:00:23 The role 'Owner' for group 'my-entra-id-group' 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 + +#### YAML file +You may define global configuration options in a YAML file. +By default, the program will use the file ~/.az-pim-cli.yaml ($HOME/.az-pim-cli.yaml), if present. You may override this path with the command line flag `--config [PATH]`. + +```bash +$ cat ~/.az-pim-cli.yaml +token: eyJ0[...] -# Activate a specific role ('Owner') in a subscription with the prefix 's398' -$ az-pim-cli activate -p s398 --role-name owner ``` + +#### Environment variables +You may also define these configuration options as environment variables by prefixing any global variable with `PIM_`. + +```bash +export PIM_TOKEN=eyJ0[...] + +``` + +### Token for Entra ID Groups +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. + +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` + - 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[...]" +4. Remove the prefix "Bearer" from the value, resulting in "eyJ0[...]" +5. Set an environment variable or config file value according to the description in [Configuration options](#configuration-options), e.g. + ``` + PIM_TOKEN=eyJ0[...] + ``` +6. You may now, and for the duration of the token's lifetime, list and activate 'Entra groups' using this tool diff --git a/cmd/activate.go b/cmd/activate.go index 88240bb..99bc61b 100644 --- a/cmd/activate.go +++ b/cmd/activate.go @@ -4,6 +4,7 @@ Copyright © 2023 netr0m package cmd import ( + "fmt" "log" "github.com/netr0m/az-pim-cli/pkg/pim" @@ -25,7 +26,7 @@ var activateCmd = &cobra.Command{ if subscriptionName == "" && subscriptionPrefix == "" { log.Fatalf("Missing required parameter: You must specify either 'subscription-name' or 'subscription-prefix'.") } - token := pim.GetPIMAccessTokenAzureCLI() + token := pim.GetPIMAccessTokenAzureCLI(pim.AZ_PIM_SCOPE) subjectId := pim.GetUserInfo(token).ObjectId eligibleRoleAssignments := pim.GetEligibleRoleAssignments(token) @@ -43,8 +44,48 @@ var activateCmd = &cobra.Command{ }, } +var activateGroupCmd = &cobra.Command{ + Use: "group [group name]", + Aliases: []string{"g", "grp", "groups"}, + Short: "Sends a request to Azure PIM to activate the given group", + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var comps []string + if len(args) == 0 { + comps = cobra.AppendActiveHelp(comps, "Please specify the name of the group") + } else if len(args) == 1 { + comps = cobra.AppendActiveHelp(comps, "This command does not take any more arguments (but may accept flags)") + } else { + comps = cobra.AppendActiveHelp(comps, "ERROR: Too many arguments specified") + } + return comps, cobra.ShellCompDirectiveNoFileComp + }, + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cobra.CheckErr(fmt.Errorf("activate group needs a name for the group")) + } + if Token == "" { + log.Fatalf("Activating a group requires providing a token manually due to restrictions in token permissions. Consult the docs for more information.") + } + subjectId := pim.GetUserInfo(Token).ObjectId + + eligibleGroupAssignments := pim.GetEligibleGroupAssignments(Token, subjectId) + groupAssignment := utils.GetGroupAssignment(args[0], roleName, eligibleGroupAssignments) + + log.Printf( + "Activating role '%s' for group '%s' with reason '%s'", + groupAssignment.RoleDefinition.DisplayName, + groupAssignment.RoleDefinition.Resource.DisplayName, + reason, + ) + + requestResponse := pim.RequestGroupAssignment(subjectId, groupAssignment, duration, reason, Token) + log.Printf("The role '%s' for group '%s' is now %s", groupAssignment.RoleDefinition.DisplayName, groupAssignment.RoleDefinition.Resource.DisplayName, requestResponse.AssignmentState) + }, +} + func init() { rootCmd.AddCommand(activateCmd) + activateCmd.AddCommand(activateGroupCmd) // Flags activateCmd.PersistentFlags().StringVarP(&subscriptionName, "subscription-name", "s", "", "The name of the subscription to activate") diff --git a/cmd/list.go b/cmd/list.go index 407865f..9a45b48 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -4,6 +4,8 @@ Copyright © 2023 netr0m package cmd import ( + "log" + "github.com/netr0m/az-pim-cli/pkg/pim" "github.com/netr0m/az-pim-cli/pkg/utils" "github.com/spf13/cobra" @@ -14,13 +16,28 @@ var listCmd = &cobra.Command{ Aliases: []string{"l", "ls"}, Short: "Query Azure PIM for eligible role assignments", Run: func(cmd *cobra.Command, args []string) { - token := pim.GetPIMAccessTokenAzureCLI() + token := pim.GetPIMAccessTokenAzureCLI(pim.AZ_PIM_SCOPE) eligibleRoleAssignments := pim.GetEligibleRoleAssignments(token) utils.PrintEligibleRoles(eligibleRoleAssignments) }, } +var listGroupCmd = &cobra.Command{ + Use: "group", + Aliases: []string{"g", "grp", "groups"}, + Short: "Query Azure PIM for eligible group assignments", + Run: func(cmd *cobra.Command, args []string) { + if Token == "" { + log.Fatalf("Listing eligible groups requires providing a token manually due to restrictions in token permissions. Consult the docs for more information.") + } + subjectId := pim.GetUserInfo(Token).ObjectId + eligibleGroupAssignments := pim.GetEligibleGroupAssignments(Token, subjectId) + utils.PrintEligibleGroups(eligibleGroupAssignments) + }, +} + func init() { rootCmd.AddCommand(listCmd) + listCmd.AddCommand(listGroupCmd) } diff --git a/cmd/root.go b/cmd/root.go index 250741d..0000e56 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,6 +14,7 @@ import ( ) var cfgFile string +var Token string var rootCmd = &cobra.Command{ Use: "az-pim-cli", @@ -36,6 +37,7 @@ func init() { // Global flags rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.az-pim-cli.yaml)") + rootCmd.PersistentFlags().StringVarP(&Token, "token", "t", "", "An access token for the PIM Groups API") } // initConfig reads in config file and ENV variables if set. diff --git a/pkg/pim/client.go b/pkg/pim/client.go index 18fed1b..2bbd25a 100644 --- a/pkg/pim/client.go +++ b/pkg/pim/client.go @@ -18,14 +18,14 @@ import ( "github.com/google/uuid" ) -func GetPIMAccessTokenAzureCLI() string { +func GetPIMAccessTokenAzureCLI(scope string) string { cred, err := azidentity.NewAzureCLICredential(nil) if err != nil { log.Fatalln(err) } tokenOpts := policy.TokenRequestOptions{ Scopes: []string{ - AZ_PIM_SCOPE, + scope, }, } token, err := cred.GetToken(context.Background(), tokenOpts) @@ -50,21 +50,19 @@ func GetUserInfo(token string) AzureUserInfo { } func Request(request *PIMRequest, responseModel any) any { - url := fmt.Sprintf("%s/%s", AZ_PIM_BASE_URL, request.Path) - // Prepare request body var req *http.Request var err error if request.Payload != nil { payload := new(bytes.Buffer) json.NewEncoder(payload).Encode(request.Payload) //nolint:errcheck - req, err = http.NewRequest(request.Method, url, payload) + req, err = http.NewRequest(request.Method, request.Url, payload) if err != nil { log.Fatalf("ERROR: %v", err) } } else { // Prepare the request - req, err = http.NewRequest(request.Method, url, nil) + req, err = http.NewRequest(request.Method, request.Url, nil) if err != nil { log.Fatalf("ERROR: %v", err) } @@ -114,7 +112,23 @@ func GetEligibleRoleAssignments(token string) *RoleAssignmentResponse { } responseModel := &RoleAssignmentResponse{} _ = Request(&PIMRequest{ - Path: fmt.Sprintf("%s/roleEligibilityScheduleInstances", AZ_PIM_BASE_PATH), + Url: fmt.Sprintf("%s/%s/roleEligibilityScheduleInstances", AZ_PIM_BASE_URL, AZ_PIM_BASE_PATH), + Token: token, + Method: "GET", + Params: params, + }, responseModel) + + return responseModel +} + +func GetEligibleGroupAssignments(token string, subjectId string) *GroupAssignmentResponse { + 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{} + _ = Request(&PIMRequest{ + Url: fmt.Sprintf("%s/%s/aadGroups/roleAssignments", AZ_PIM_GROUP_BASE_URL, AZ_PIM_GROUP_BASE_PATH), Token: token, Method: "GET", Params: params, @@ -136,8 +150,9 @@ func ValidateRoleAssignmentRequest(scope string, roleAssignmentRequest RoleAssig validationResponse := &RoleAssignmentRequestResponse{} _ = Request(&PIMRequest{ - Path: fmt.Sprintf( - "%s/%s/roleAssignmentScheduleRequests/%s/validate", + Url: fmt.Sprintf( + "%s/%s/%s/roleAssignmentScheduleRequests/%s/validate", + AZ_PIM_BASE_URL, scope, AZ_PIM_BASE_PATH, uuid.NewString(), @@ -164,6 +179,41 @@ func ValidateRoleAssignmentRequest(scope string, roleAssignmentRequest RoleAssig return false } +func ValidateGroupAssignmentRequest(groupAssignmentRequest GroupAssignmentRequest, token string) bool { + var params = map[string]string{ + "evaluateOnly": "true", + } + + groupAssignmentValidationRequest := groupAssignmentRequest + groupAssignmentValidationRequest.Reason = "Evaluate Only" + groupAssignmentValidationRequest.TicketNumber = "Evaluate Only" + groupAssignmentValidationRequest.TicketSystem = "Evaluate Only" + + validationResponse := &GroupAssignmentRequestResponse{} + _ = Request(&PIMRequest{ + Url: fmt.Sprintf("%s/%s/aadGroups/roleAssignmentRequests", AZ_PIM_GROUP_BASE_URL, AZ_PIM_GROUP_BASE_PATH), + Token: token, + Method: "POST", + Params: params, + Payload: groupAssignmentValidationRequest, + }, validationResponse) + + if IsGroupAssignmentRequestFailed(validationResponse) { + log.Printf("ERROR: The group assignment validation failed with status '%s', '%s'", validationResponse.Status.Status, validationResponse.Status.SubStatus) + log.Fatalln(validationResponse) + return false + } + if IsGroupAssignmentRequestOK(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) + return true + } + + return false +} + func RequestRoleAssignment(subjectId string, roleAssignment *RoleAssignment, duration int, reason string, token string) *RoleAssignmentRequestResponse { var params = map[string]string{ "api-version": AZ_PIM_API_VERSION, @@ -194,8 +244,9 @@ func RequestRoleAssignment(subjectId string, roleAssignment *RoleAssignment, dur responseModel := &RoleAssignmentRequestResponse{} _ = Request(&PIMRequest{ - Path: fmt.Sprintf( - "%s/%s/roleAssignmentScheduleRequests/%s", + Url: fmt.Sprintf( + "%s/%s/%s/roleAssignmentScheduleRequests/%s", + AZ_PIM_BASE_URL, scope, AZ_PIM_BASE_PATH, uuid.NewString(), @@ -208,3 +259,36 @@ func RequestRoleAssignment(subjectId string, roleAssignment *RoleAssignment, dur return responseModel } + +func RequestGroupAssignment(subjectId string, groupAssignment *GroupAssignment, duration int, reason string, token string) *GroupAssignmentRequestResponse { + groupAssignmentRequest := &GroupAssignmentRequest{ + RoleDefinitionId: groupAssignment.RoleDefinitionId, + ResourceId: groupAssignment.ResourceId, + SubjectId: subjectId, + AssignmentState: "Active", + Type: "UserAdd", + Reason: reason, + TicketNumber: "", + TicketSystem: "az-pim-cli", + Schedule: &GroupAssignmentSchedule{ + Type: "Once", + StartDateTime: nil, + EndDateTime: nil, + Duration: fmt.Sprintf("PT%dM", duration), + }, + LinkedEligibleRoleAssignmentId: groupAssignment.Id, + ScopedResourceId: "", + } + + ValidateGroupAssignmentRequest(*groupAssignmentRequest, token) + + responseModel := &GroupAssignmentRequestResponse{} + _ = Request(&PIMRequest{ + Url: fmt.Sprintf("%s/%s/aadGroups/roleAssignmentRequests", AZ_PIM_GROUP_BASE_URL, AZ_PIM_GROUP_BASE_PATH), + Token: token, + Method: "POST", + Payload: groupAssignmentRequest, + }, responseModel) + + return responseModel +} diff --git a/pkg/pim/const.go b/pkg/pim/const.go index cd6072b..76d00b7 100644 --- a/pkg/pim/const.go +++ b/pkg/pim/const.go @@ -6,9 +6,15 @@ 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 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" + // Authority used for Azure authentication const AZ_AUTHORITY string = "https://login.microsoftonline.com/" diff --git a/pkg/pim/models.go b/pkg/pim/models.go index b0d8fec..d808318 100644 --- a/pkg/pim/models.go +++ b/pkg/pim/models.go @@ -17,7 +17,7 @@ type AzureUserInfoClaims struct { } type PIMRequest struct { - Path string + Url string Token string Method string Headers map[string][]string @@ -65,6 +65,44 @@ type RoleAssignmentResponse struct { Value []RoleAssignment `json:"value"` } +type GroupAssignmentSubject struct { + Id string `json:"id"` + Type string `json:"type"` + DisplayName string `json:"displayName"` + PrincipalName string `json:"principalName"` + Email string `json:"email"` +} + +type GroupResource 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 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 GroupAssignmentResponse struct { + Value []GroupAssignment `json:"value"` +} + type TicketInfo struct { TicketNumber string `json:"ticketNumber"` TicketSystem string `json:"ticketSystem"` @@ -145,3 +183,53 @@ type RoleAssignmentRequestProperties struct { type RoleAssignmentRequestRequest struct { Properties RoleAssignmentRequestProperties `json:"Properties"` } + +type GroupAssignmentSchedule 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 GroupAssignmentRequestStatus 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"` +} diff --git a/pkg/pim/utils.go b/pkg/pim/utils.go index 3504d58..efbe22e 100644 --- a/pkg/pim/utils.go +++ b/pkg/pim/utils.go @@ -11,6 +11,14 @@ func IsRoleAssignmentRequestFailed(requestResponse *RoleAssignmentRequestRespons return false } +func IsGroupAssignmentRequestFailed(requestResponse *GroupAssignmentRequestResponse) bool { + switch requestResponse.Status.SubStatus { + case StatusAdminDenied, StatusCanceled, StatusDenied, StatusFailed, StatusFailedAsResourceIsLocked, StatusInvalid, StatusRevoked, StatusTimedOut: + return true + } + return false +} + func IsRoleAssignmentRequestPending(requestResponse *RoleAssignmentRequestResponse) bool { switch requestResponse.Properties.Status { case StatusPendingAdminDecision, StatusPendingApproval, StatusPendingApprovalProvisioning, StatusPendingEvaluation, StatusPendingExternalProvisioning, StatusPendingProvisioning, StatusPendingRevocation, StatusPendingScheduleCreation: @@ -19,6 +27,14 @@ func IsRoleAssignmentRequestPending(requestResponse *RoleAssignmentRequestRespon return false } +func IsGroupAssignmentRequestPending(requestResponse *GroupAssignmentRequestResponse) bool { + switch requestResponse.Status.SubStatus { + case StatusPendingAdminDecision, StatusPendingApproval, StatusPendingApprovalProvisioning, StatusPendingEvaluation, StatusPendingExternalProvisioning, StatusPendingProvisioning, StatusPendingRevocation, StatusPendingScheduleCreation: + return true + } + return false +} + func IsRoleAssignmentRequestOK(requestResponse *RoleAssignmentRequestResponse) bool { switch requestResponse.Properties.Status { case StatusAccepted, StatusAdminApproved, StatusGranted, StatusProvisioned, StatusProvisioningStarted, StatusScheduleCreated: @@ -26,3 +42,11 @@ func IsRoleAssignmentRequestOK(requestResponse *RoleAssignmentRequestResponse) b } return false } + +func IsGroupAssignmentRequestOK(requestResponse *GroupAssignmentRequestResponse) bool { + switch requestResponse.Status.SubStatus { + case StatusAccepted, StatusAdminApproved, StatusGranted, StatusProvisioned, StatusProvisioningStarted, StatusScheduleCreated: + return true + } + return false +} diff --git a/pkg/utils/main.go b/pkg/utils/main.go index 9168b75..de4f30d 100644 --- a/pkg/utils/main.go +++ b/pkg/utils/main.go @@ -31,6 +31,26 @@ func PrintEligibleRoles(roleEligibilityScheduleInstances *pim.RoleAssignmentResp } } +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{} + } + eligibleGroups[groupName] = append(eligibleGroups[groupName], roleName) + } + + for grp, rol := range eligibleGroups { + fmt.Printf("== %s ==\n", grp) + for role := range rol { + fmt.Printf("\t - %s\n", rol[role]) + } + } +} + func GetRoleAssignment(name string, prefix string, role string, eligibleRoleAssignments *pim.RoleAssignmentResponse) *pim.RoleAssignment { for _, eligibleRoleAssignment := range eligibleRoleAssignments.Value { var match *pim.RoleAssignment = nil @@ -63,3 +83,24 @@ func GetRoleAssignment(name string, prefix string, role string, eligibleRoleAssi return nil } + +func GetGroupAssignment(name string, role string, eligibleGroupAssignments *pim.GroupAssignmentResponse) *pim.GroupAssignment { + name = strings.ToLower(name) + for _, eligibleGroupAssignment := range eligibleGroupAssignments.Value { + currentGroupName := strings.ToLower(eligibleGroupAssignment.RoleDefinition.Resource.DisplayName) + + if currentGroupName == name { + if role == "" { + return &eligibleGroupAssignment + } + role = strings.ToLower(role) + if strings.Contains(strings.ToLower(eligibleGroupAssignment.RoleDefinition.DisplayName), role) { + return &eligibleGroupAssignment + } + } + } + + log.Fatalln("Unable to find a group assignment matching the parameters.") + + return nil +}