Search Role Members

This is in text format, but taken directly from my PS script. Takes an hour to run for 3300 identities.

function Get-AccessItemIdentityMapping {
  param (
    [string]$tenant,
    [hashtable]$headers
  )

  # Record the start time for performance tracking
  $startTime = Get-Date
  # Initialize hashtables to store mappings of roles/access profiles to identities
  $roleIdentities = @{}
  $accessProfileIdentities = @{}

  # --- Step 1: Get all Roles and Access Profiles from the Identity Platform ---
  Write-Host "Fetching all Roles..."
  # A hashtable to keep track of processed role names
  $rolesToProcess = @{}
  # Define the query body for fetching roles
  $roleQueryBody = @{
    indices           = @("roles") # Target the 'roles' index
    query             = @{ query = "enabled:true" } # Filter for enabled roles
    queryResultFilter = @{ includes = @("id", "name") } # Only retrieve ID and name
  }
  try {
    # Invoke REST method to search for roles. Limit is set to 4000 to retrieve all roles.
    $roleResponse = Invoke-RestMethod -Uri "https://$tenant.api.identitynow.com/v2024/search?limit=4000" -Method 'POST' -Headers $headers -Body ($roleQueryBody | ConvertTo-Json -Depth 10) -ErrorAction Stop
    Write-Host "Fetched $($roleResponse.Count) roles."
    # Populate rolesToProcess with role names for efficient lookup
    foreach ($role in $roleResponse) {
      $rolesToProcess[$role.name] = $true
    }
  }
  catch {
    # Error handling for role fetching
    Write-Error "Error fetching roles: $($_.Exception.Message)"
    return $null, $null # Return nulls if an error occurs
  }

  Write-Host "Fetching all Access Profiles..."
  # A hashtable to keep track of processed access profile names
  $accessProfilesToProcess = @{}
  # Define the query body for fetching access profiles
  $accessProfileQueryBody = @{
    indices           = @("accessprofiles") # Target the 'accessprofiles' index
    query             = @{ query = "enabled:true" } # Filter for enabled access profiles
    queryResultFilter = @{includes = @("id", "name") } # Only retrieve ID and name
  }
  try {
    # Invoke REST method to search for access profiles. Limit is set to 4000 to retrieve all profiles.
    $accessProfileResponse = Invoke-RestMethod -Uri "https://$tenant.api.identitynow.com/v2024/search?limit=4000" -Method 'POST' -Headers $headers -Body ($accessProfileQueryBody | ConvertTo-Json -Depth 10) -ErrorAction Stop
    Write-Host "Fetched $($accessProfileResponse.Count) access profiles."
    # Populate accessProfilesToProcess with access profile names for efficient lookup
    foreach ($accessProfile in $accessProfileResponse) {
      $accessProfilesToProcess[$accessProfile.name] = $true
    }
  }
  catch {
    # Error handling for access profile fetching
    Write-Error "Error fetching access profiles: $($_.Exception.Message)"
    return $null, $null # Return nulls if an error occurs
  }

  # --- Step 2: Fetch all Identities and their Assigned Access ---
  Write-Host "Fetching all Identities and their assigned Access..."
  # Define the query body for fetching identities with pagination
  $identityQueryBody = @{
    indices           = @("identities") # Target the 'identities' index
    query             = @{
      query = "attributes.cloudLifecycleState:active" # Filter for active identities
      sort  = @(
        @{ field = "id"; order = "asc" } # Sort by ID for consistent pagination
      )
    }
    # Updated to include 'attributes.email' instead of 'attributes.adUsername'
    queryResultFilter = @{includes = @("id", "displayName", "attributes.email", "access.name", "access.type") } # Specify required fields
    offset            = 0 # Starting offset for pagination
    limit             = 300 # Number of identities to fetch per request
  }
  $identitiesProcessedCount = 0 # Counter for processed identities
  $continueIdentities = $true # Flag to control pagination loop

  while ($continueIdentities) {
    Write-Host "Fetching identities... Offset: $($identityQueryBody.offset), Processed: $identitiesProcessedCount"

    try {
      # Invoke REST method to search for identities with pagination
      $identityResponse = Invoke-RestMethod -Uri "https://$tenant.api.identitynow.com/v3/search?offset=$($identityQueryBody.offset)&limit=$($identityQueryBody.limit)" -Method 'POST' -Headers $headers -Body ($identityQueryBody | ConvertTo-Json -Depth 10) -ErrorAction Stop

      # Process each identity in the current response
      foreach ($identity in $identityResponse) {
        $identitiesProcessedCount++
        # Extract the email attribute
        $email = $identity.attributes.email

        # Iterate through known roles and check if the identity has them
        foreach ($roleName in $rolesToProcess.Keys) {
          foreach ($access in $identity.access) {
            # If the access type is 'ROLE' and the name matches a known role
            if ($access.type -ceq "ROLE" -and $access.name -ceq $roleName) {
              # Initialize the array for the role if it doesn't exist
              if (-not $roleIdentities.ContainsKey($roleName)) {
                $roleIdentities[$roleName] = @()
              }
              # Add the email to the role's identity list if available
              if ($email) {
                $roleIdentities[$roleName] += $email
              }
              break # Move to the next role for this identity once found
            }
          }
        }

        # Iterate through known access profiles and check if the identity has them
        foreach ($accessProfileName in $accessProfilesToProcess.Keys) {
          foreach ($access in $identity.access) {
            # If the access type is 'ACCESS_PROFILE' and the name matches a known access profile
            if ($access.type -ceq "ACCESS_PROFILE" -and $access.name -ceq $accessProfileName) {
              # Initialize the array for the access profile if it doesn't exist
              if (-not $accessProfileIdentities.ContainsKey($accessProfileName)) {
                $accessProfileIdentities[$accessProfileName] = @()
              }
              # Add the email to the access profile's identity list if available
              if ($email) {
                $accessProfileIdentities[$accessProfileName] += $email
              }
              break # Move to the next access profile for this identity once found
            }
          }
        }
      }

      # Check if all identities have been fetched (response count less than limit)
      if ($identityResponse.Count -lt $($identityQueryBody.limit)) {
        $continueIdentities = $false # Stop the pagination loop
      }
      # Increment the offset for the next paginated request
      $identityQueryBody.offset += $identityQueryBody.limit

    }
    catch {
      # Error handling for identity fetching
      Write-Error "Error fetching identities: $($_.Exception.Message)"
      $continueIdentities = $false # Stop the loop on error
    }
  }

  Write-Host "Finished processing $identitiesProcessedCount identities."

  # --- Step 3: Prepare data for CSV Export ---
  # Create an array to hold role data for CSV export
  $roleExportData = @()
  foreach ($role in $roleIdentities.Keys) {
    # Get unique emails for the current role and sort them
    $emails = ($roleIdentities[$role] | Sort-Object -Unique)
    $count = $emails.Count # Count of unique users
    $emailsString = $emails -join ";" # Join emails with a semicolon
    # Create a custom object for the CSV row
    $roleExportData += [PSCustomObject]@{
      "Role Name" = $role
      "Count"     = $count
      "Emails"    = $emailsString # Changed column name to 'Emails'
    }
  }

  # Create an array to hold access profile data for CSV export
  $accessProfileExportData = @()
  foreach ($accessProfile in $accessProfileIdentities.Keys) {
    # Get unique emails for the current access profile and sort them
    $emails = ($accessProfileIdentities[$accessProfile] | Sort-Object -Unique)
    $count = $emails.Count # Count of unique users
    $emailsString = $emails -join ";" # Join emails with a semicolon
    # Create a custom object for the CSV row
    $accessProfileExportData += [PSCustomObject]@{
      "Access Profile Name" = $accessProfile
      "Count"               = $count
      "Emails"              = $emailsString # Changed column name to 'Emails'
    }
  }

  # Calculate and display the total time taken for the function
  $endTime = Get-Date
  $timeTaken = $endTime - $startTime
  Write-Host "Function Get-AccessItemIdentityMapping took $($timeTaken.TotalSeconds) seconds to run."

  # Return the two data sets
  return $roleExportData, $accessProfileExportData
}

# This script retrieves role and access profile identity mappings from the IdentityNow API.

## Script Execution Section
# Define your tenant identifier (e.g., "yourtenant")
$Tenant = "yourtenantname"

# Define your Client ID and Client Secret for API authentication.
# **IMPORTANT: In a production environment, avoid hardcoding these values directly in the script.**
# Consider using secure methods like Azure Key Vault, HashiCorp Vault, or PowerShell's SecretManagement module.
$clientID = "YOUR_CLIENT_ID"
$clientSecret = "YOUR_CLIENT_SECRET"

# Function to obtain an OAuth 2.0 access token
function Get-AccessToken {
  param (
    [String]$tenant,
    [String]$clientID,
    [String]$clientSecret
  )
  # Parameters for the REST call to the OAuth token endpoint
  $params = @{
    uri    = "https://$tenant.api.identitynow.com/oauth/token?grant_type=client_credentials&client_id=$($clientID)&client_secret=$($clientSecret)"
    method = "POST"
  }
  # Invoke the REST method and return the access token
  return (Invoke-RestMethod @params).access_token
}

# Get the access token required for subsequent API calls
$token = Get-AccessToken -tenant $tenant -clientID $clientID -clientSecret $clientSecret

# Define the standard HTTP headers for API requests
$headers = @{
  "Content-Type"  = "application/json"
  "Accept"        = "application/json"
  "Authorization" = "Bearer $token" # Include the acquired access token
}

# Execute the main function to retrieve role and access profile identity mappings
$roleReport, $accessProfileReport = Get-AccessItemIdentityMapping -tenant $tenant -headers $headers

# Define the output path for CSV files
# **IMPORTANT: Adjust this path to a location accessible on your system.**
$outputPath = "C:\Temp\IdentityReports"

# Ensure the output directory exists
if (-not (Test-Path $outputPath)) {
  New-Item -Path $outputPath -ItemType Directory -Force | Out-Null
}

# Export Role results to a CSV file
if ($roleReport) {
  # Include a timestamp in the filename to avoid overwriting previous reports
  $roleCsvFilePath = Join-Path $outputPath "Roles_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
  $roleReport | Export-Csv -Path $roleCsvFilePath -NoTypeInformation
  Write-Host "Role data exported to $roleCsvFilePath"
}
else {
  Write-Warning "No role data retrieved."
}

# Export Access Profile results to a CSV file
if ($accessProfileReport) {
  # Include a timestamp in the filename to avoid overwriting previous reports
  $accessProfileCsvFilePath = Join-Path $outputPath "AccessProfiles_$(Get-Date -Format 'ddMMyyyy').csv"
  $accessProfileReport | Export-Csv -Path $accessProfileCsvFilePath -NoTypeInformation
  Write-Host "Access Profile data exported to $accessProfileCsvFilePath"
}
else {
  Write-Warning "No access profile data retrieved."
}
1 Like