From 1cd42b1073739ffdb7b137bc7c693be2b89987d3 Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Thu, 27 Feb 2025 10:34:51 +0300 Subject: [PATCH 1/9] Initial commit --- .../Assign-EntraAppRoleToApplicationUsers.ps1 | 347 ++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 diff --git a/module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 b/module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 new file mode 100644 index 0000000000..9959543879 --- /dev/null +++ b/module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 @@ -0,0 +1,347 @@ +function Assign-EntraAppRoleToApplicationUsers { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic'")] + [ValidateSet("DatabaseorDirectory", "SAPCloudIdentity", "Generic")] + [string]$DataSource, + + [Parameter(Mandatory = $true, HelpMessage = "Path to the input file containing users, e.g., C:\temp\users.csv")] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $_ })] + [string]$FileName, + + [Parameter(Mandatory = $true, HelpMessage = "Name of the application (Service Principal) to assign roles for")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationName + ) + + process { + + # Call the orchestrator function + Write-Verbose "Service principal already exists for application with params: AppName - " $ApplicationName" | FileName - " $FileName" | DataSource - " $DataSource + + Main + + function Main() { + + try { + # Import users from the CSV file + $users = Import-Csv -Path $FileName + if (-not $users) { + Write-Error "No users found in the provided file: $FileName" + return + } + + # Define the property name for user lookup based on data source + $sourceMatchPropertyName = switch ($DataSource) { + "DatabaseorDirectory" { "email" } + "SAPCloudIdentity" { "userName" } + "Generic" { "userPrincipalName" } + } + + # Get or create the application and service principal once + $application = Get-OrCreateEntraApplication -DisplayName $ApplicationName + if (-not $application) { + Write-Error "Failed to retrieve or create application: $ApplicationName" + return + } + + # Get unique roles from users - commenting out this part due to creation error + <# $uniqueRoles = $users | + Where-Object { -not [string]::IsNullOrWhiteSpace($_.Role) } | + Select-Object -ExpandProperty Role -Unique + + if (-not $uniqueRoles) { + Write-Error "No roles found in the input file" + return + } + + Write-Verbose "Found $($uniqueRoles.Count) unique roles to process" + + # Create app roles for each unique role + $createdRoles = @{} + foreach ($role in $uniqueRoles) { + $cleanRoleName = Clean-PropertyValue -Value $role -PropertyName "Role" + if (-not $cleanRoleName) { continue } + + $newRole = New-AppRoleIfNotExists -ApplicationId $application.ApplicationId -RoleDisplayName $cleanRoleName + if ($newRole) { + $createdRoles[$cleanRoleName] = $newRole + Write-Verbose "Created/Retrieved role: $cleanRoleName" + } + } #> + + + # Process users in bulk where possible + foreach ($user in $users) { + $cleanUserPrincipalName = Clean-PropertyValue -Value $user.$sourceMatchPropertyName -PropertyName $sourceMatchPropertyName + if (-not $cleanUserPrincipalName) { + Write-Warning "Skipping user due to invalid sourceMatchPropertyName: $($user.$sourceMatchPropertyName)" + continue + } + + $cleanDisplayName = Clean-PropertyValue -Value $user.displayName -PropertyName "displayName" + + if (-not $cleanDisplayName) { + Write-Warning "Skipping user due to invalid sourceMatchPropertyName: $($user.$DisplayName)" + continue + } + $cleanMailNickname = Clean-PropertyValue -Value $user.mailNickname -PropertyName "mailNickname" + + if (-not $cleanMailNickname) { + Write-Warning "Skipping user due to invalid sourceMatchPropertyName: $($user.$MailNickname)" + continue + } + + # Get the user's role + $userRole = Clean-PropertyValue -Value $user.Role -PropertyName "Role" + if (-not $userRole -or -not $createdRoles.ContainsKey($userRole)) { + Write-Warning "Invalid or unmapped role for user $($userInfo.UserPrincipalName): $($user.Role)" + continue + } + + + # Create user if they do not exist + $userInfo = New-EntraUserIfNotExists -UserPrincipalName $cleanUserPrincipalName -DisplayName $cleanDisplayName -MailNickname $cleanMailNickname + if (-not $userInfo) { continue } + + # Assign roles to the user (Placeholder for role assignment logic) + + $assignment = Assign-AppServicePrincipalRoleAssignmentIfNotExists -ServicePrincipalId $application.ServicePrincipalId -UserId $userInfo.Id -ApplicationName $ApplicationName -RoleDisplayName $userRole + if (-not $assignment) { continue } + Write-Verbose "Assigning roles to user $($userInfo.UserPrincipalName) in application $ApplicationName" + } + } + catch { + Write-Error "Error in Start-Orchestration: $_" + } + } + + function New-EntraUserIfNotExists($UserPrincipalName, $DisplayName, $MailNickname) { + + try { + # Check if user exists + $existingUser = Get-EntraUser -Filter "userPrincipalName eq '$UserPrincipalName'" -ErrorAction SilentlyContinue + if ($existingUser) { + Write-Verbose "User exists: $UserPrincipalName" + return $existingUser + } + + # Create new user + $params = @{ + UserPrincipalName = $UserPrincipalName + DisplayName = ($UserPrincipalName -split '@')[0] + MailNickname = ($UserPrincipalName -split '@')[0] + AccountEnabled = $false + PasswordProfile = @{ + Password = -join (((48..90) + (96..122)) * 16 | Get-Random -Count 16 | % { [char]$_ }) + ForceChangePasswordNextSignIn = $true + } + } + + $newUser = New-EntraUser @params + Write-Verbose "Created new user: $UserPrincipalName" + return $newUser + } + catch { + Write-Error "Failed to create or verify user $($UserPrincipalName): $_" + return $null + } + } + + function Clean-PropertyValue($Value, $PropertyName) { + + try { + if ([string]::IsNullOrWhiteSpace($Value)) { + Write-Warning "Invalid $PropertyName value" + return $null + } + + # Remove unwanted characters and normalize + $cleanValue = $Value -replace '[^\w\s@\.-]', '' -replace '\s+', ' ' | ForEach-Object { $_.Trim().ToLower() } + + return if ([string]::IsNullOrWhiteSpace($cleanValue)) { + Write-Warning "$PropertyName is empty after cleanup" + $null + } else { $cleanValue } + } + catch { + Write-Error "Error cleaning $($PropertyName): $($_)" + return $null + } + } + + function Get-OrCreateEntraApplication($DisplayName) { + + try { + # Check if application exists + $existingApp = Get-EntraApplication -Filter "displayName eq '$DisplayName'" -ErrorAction SilentlyContinue + + if (-not $existingApp) { + # Create new application + $appParams = @{ + DisplayName = $DisplayName + Description = $DisplayName + SignInAudience = "AzureADMyOrg" + Web = @{ + RedirectUris = @("https://localhost") + } + } + + $newApp = New-EntraApplication @appParams + Write-Verbose "Created new application: $DisplayName" + + # Create service principal for the application + $spParams = @{ + AppId = $newApp.AppId + DisplayName = $DisplayName + } + + $newSp = New-EntraServicePrincipal @spParams + Write-Verbose "Created new service principal for application: $DisplayName" + + [PSCustomObject]@{ + ApplicationId = $newApp.Id + ApplicationDisplayName = $newApp.DisplayName + ServicePrincipalId = $newSp.Id + ServicePrincipalDisplayName = $newSp.DisplayName + Status = 'Created' + } + } + else { + # Get existing service principal + $existingSp = Get-EntraServicePrincipal -Filter "appId eq '$($existingApp.AppId)'" -ErrorAction SilentlyContinue + + if (-not $existingSp) { + # Create service principal if it doesn't exist + $spParams = @{ + AppId = $existingApp.AppId + DisplayName = $DisplayName + } + + $newSp = New-EntraServicePrincipal @spParams + Write-Verbose "Created new service principal for existing application: $DisplayName" + } + else { + $newSp = $existingSp + Write-Verbose "Service principal already exists for application: $DisplayName" + } + + [PSCustomObject]@{ + ApplicationId = $existingApp.Id + ApplicationDisplayName = $existingApp.DisplayName + ServicePrincipalId = $newSp.Id + ServicePrincipalDisplayName = $newSp.DisplayName + Status = 'Exists' + } + } + } + catch { + Write-Error "Error in Get-OrCreateEntraApplication: $_" + return $null + } + } + + function New-AppRoleIfNotExists($ApplicationId, $RoleDisplayName) { + + try { + # Get existing application + $application = Get-EntraApplication -ApplicationId $ApplicationId -ErrorAction Stop + if (-not $application) { + Write-Error "Application not found with ID: $ApplicationId" + return $null + } + + $AllowedMemberTypes = "User" + + # Create new app role + $appRole = @{ + AllowedMemberTypes = $AllowedMemberTypes + Description = $RoleDisplayName + DisplayName = $RoleDisplayName + Id = [guid]::NewGuid() + IsEnabled = $true + Value = $RoleValue ?? $RoleDisplayName + } + + # Get existing roles and add new role + $existingRoles = $application.AppRoles ?? @() + + # Check if role already exists + if ($existingRoles.Where({ $_.DisplayName -eq $RoleDisplayName })) { + Write-Warning "Role '$RoleDisplayName' already exists in application" + return $null + } + + $updatedRoles = $existingRoles + $appRole + + # Update application with new role + $params = @{ + ApplicationId = $ApplicationId + AppRoles = $updatedRoles + Tags = @("WindowsAzureActiveDirectoryIntegratedApp") + } + + $updatedApp = Set-EntraApplication @params + + Write-Verbose "Created new role '$RoleDisplayName' in application '$($application.DisplayName)'" + + # Return the newly created role + return [PSCustomObject]@{ + ApplicationId = $ApplicationId + RoleId = $appRole.Id + DisplayName = $RoleDisplayName + Description = $RoleDescription + Value = $appRole.Value + IsEnabled = $true + } + } + catch { + Write-Error "Failed to create app role: $_" + return $null + } + } + + function Assign-AppServicePrincipalRoleAssignmentIfNotExists($ServicePrincipalId, $UserId, $ApplicationName, $RoleDisplayName) { + + try { + # Check if assignment exists + + $servicePrincipalObject = Get-EntraServicePrincipal -ServicePrincipalId $ServicePrincipalId + $appRoleId = $servicePrincipalObject.AppRoles | Where-Object { $_.displayName -eq $RoleDisplayName } | Select-Object -Property Id + + $existingAssignment = Get-EntraServicePrincipalAppRoleAssignedTo -ServicePrincipalId $ServicePrincipalId | Where-Object { $_.AppRoleId -eq $appRoleId } -ErrorAction SilentlyContinue + + if ($existingAssignment) { + Write-Verbose "Role assignment already exists for user '$ApplicationName' with role '$RoleDisplayName'" + + return [PSCustomObject]@{ + ServicePrincipalId = $ServicePrincipalId + PrincipalId = $UserId + AppRoleId = $appRoleId + Status = 'Exists' + } + } + + # Create new assignment + + $newAssignment = New-EntraServicePrincipalAppRoleAssignment -ServicePrincipalId $ServicePrincipalId -ResourceId $ServicePrincipalId -Id $appRoleId -PrincipalId $UserId + Write-Verbose "Created new role assignment for user '$UserId' - AppName: '$ApplicationName' with role '$RoleDisplayName'" + + return [PSCustomObject]@{ + ServicePrincipalId = $ServicePrincipalId + PrincipalId = $UserId + AppRoleId = $appRoleId + Status = 'Created' + } + } + catch { + Write-Error "Failed to create or verify role assignment: $_" + return $null + } + } + + } + +} + From 0be4d8dc0c088146c27bb6f3326178218c2c6533 Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Fri, 28 Feb 2025 11:39:21 +0300 Subject: [PATCH 2/9] Add export capability --- .../Assign-EntraAppRoleToApplicationUsers.ps1 | 347 ------------ .../Set-EntraAppRoleToApplicationUser.ps1 | 529 ++++++++++++++++++ 2 files changed, 529 insertions(+), 347 deletions(-) delete mode 100644 module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 create mode 100644 module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 diff --git a/module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 b/module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 deleted file mode 100644 index 9959543879..0000000000 --- a/module/Entra/Microsoft.Entra/Governance/Assign-EntraAppRoleToApplicationUsers.ps1 +++ /dev/null @@ -1,347 +0,0 @@ -function Assign-EntraAppRoleToApplicationUsers { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true, HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic'")] - [ValidateSet("DatabaseorDirectory", "SAPCloudIdentity", "Generic")] - [string]$DataSource, - - [Parameter(Mandatory = $true, HelpMessage = "Path to the input file containing users, e.g., C:\temp\users.csv")] - [ValidateNotNullOrEmpty()] - [ValidateScript({ Test-Path $_ })] - [string]$FileName, - - [Parameter(Mandatory = $true, HelpMessage = "Name of the application (Service Principal) to assign roles for")] - [ValidateNotNullOrEmpty()] - [string]$ApplicationName - ) - - process { - - # Call the orchestrator function - Write-Verbose "Service principal already exists for application with params: AppName - " $ApplicationName" | FileName - " $FileName" | DataSource - " $DataSource - - Main - - function Main() { - - try { - # Import users from the CSV file - $users = Import-Csv -Path $FileName - if (-not $users) { - Write-Error "No users found in the provided file: $FileName" - return - } - - # Define the property name for user lookup based on data source - $sourceMatchPropertyName = switch ($DataSource) { - "DatabaseorDirectory" { "email" } - "SAPCloudIdentity" { "userName" } - "Generic" { "userPrincipalName" } - } - - # Get or create the application and service principal once - $application = Get-OrCreateEntraApplication -DisplayName $ApplicationName - if (-not $application) { - Write-Error "Failed to retrieve or create application: $ApplicationName" - return - } - - # Get unique roles from users - commenting out this part due to creation error - <# $uniqueRoles = $users | - Where-Object { -not [string]::IsNullOrWhiteSpace($_.Role) } | - Select-Object -ExpandProperty Role -Unique - - if (-not $uniqueRoles) { - Write-Error "No roles found in the input file" - return - } - - Write-Verbose "Found $($uniqueRoles.Count) unique roles to process" - - # Create app roles for each unique role - $createdRoles = @{} - foreach ($role in $uniqueRoles) { - $cleanRoleName = Clean-PropertyValue -Value $role -PropertyName "Role" - if (-not $cleanRoleName) { continue } - - $newRole = New-AppRoleIfNotExists -ApplicationId $application.ApplicationId -RoleDisplayName $cleanRoleName - if ($newRole) { - $createdRoles[$cleanRoleName] = $newRole - Write-Verbose "Created/Retrieved role: $cleanRoleName" - } - } #> - - - # Process users in bulk where possible - foreach ($user in $users) { - $cleanUserPrincipalName = Clean-PropertyValue -Value $user.$sourceMatchPropertyName -PropertyName $sourceMatchPropertyName - if (-not $cleanUserPrincipalName) { - Write-Warning "Skipping user due to invalid sourceMatchPropertyName: $($user.$sourceMatchPropertyName)" - continue - } - - $cleanDisplayName = Clean-PropertyValue -Value $user.displayName -PropertyName "displayName" - - if (-not $cleanDisplayName) { - Write-Warning "Skipping user due to invalid sourceMatchPropertyName: $($user.$DisplayName)" - continue - } - $cleanMailNickname = Clean-PropertyValue -Value $user.mailNickname -PropertyName "mailNickname" - - if (-not $cleanMailNickname) { - Write-Warning "Skipping user due to invalid sourceMatchPropertyName: $($user.$MailNickname)" - continue - } - - # Get the user's role - $userRole = Clean-PropertyValue -Value $user.Role -PropertyName "Role" - if (-not $userRole -or -not $createdRoles.ContainsKey($userRole)) { - Write-Warning "Invalid or unmapped role for user $($userInfo.UserPrincipalName): $($user.Role)" - continue - } - - - # Create user if they do not exist - $userInfo = New-EntraUserIfNotExists -UserPrincipalName $cleanUserPrincipalName -DisplayName $cleanDisplayName -MailNickname $cleanMailNickname - if (-not $userInfo) { continue } - - # Assign roles to the user (Placeholder for role assignment logic) - - $assignment = Assign-AppServicePrincipalRoleAssignmentIfNotExists -ServicePrincipalId $application.ServicePrincipalId -UserId $userInfo.Id -ApplicationName $ApplicationName -RoleDisplayName $userRole - if (-not $assignment) { continue } - Write-Verbose "Assigning roles to user $($userInfo.UserPrincipalName) in application $ApplicationName" - } - } - catch { - Write-Error "Error in Start-Orchestration: $_" - } - } - - function New-EntraUserIfNotExists($UserPrincipalName, $DisplayName, $MailNickname) { - - try { - # Check if user exists - $existingUser = Get-EntraUser -Filter "userPrincipalName eq '$UserPrincipalName'" -ErrorAction SilentlyContinue - if ($existingUser) { - Write-Verbose "User exists: $UserPrincipalName" - return $existingUser - } - - # Create new user - $params = @{ - UserPrincipalName = $UserPrincipalName - DisplayName = ($UserPrincipalName -split '@')[0] - MailNickname = ($UserPrincipalName -split '@')[0] - AccountEnabled = $false - PasswordProfile = @{ - Password = -join (((48..90) + (96..122)) * 16 | Get-Random -Count 16 | % { [char]$_ }) - ForceChangePasswordNextSignIn = $true - } - } - - $newUser = New-EntraUser @params - Write-Verbose "Created new user: $UserPrincipalName" - return $newUser - } - catch { - Write-Error "Failed to create or verify user $($UserPrincipalName): $_" - return $null - } - } - - function Clean-PropertyValue($Value, $PropertyName) { - - try { - if ([string]::IsNullOrWhiteSpace($Value)) { - Write-Warning "Invalid $PropertyName value" - return $null - } - - # Remove unwanted characters and normalize - $cleanValue = $Value -replace '[^\w\s@\.-]', '' -replace '\s+', ' ' | ForEach-Object { $_.Trim().ToLower() } - - return if ([string]::IsNullOrWhiteSpace($cleanValue)) { - Write-Warning "$PropertyName is empty after cleanup" - $null - } else { $cleanValue } - } - catch { - Write-Error "Error cleaning $($PropertyName): $($_)" - return $null - } - } - - function Get-OrCreateEntraApplication($DisplayName) { - - try { - # Check if application exists - $existingApp = Get-EntraApplication -Filter "displayName eq '$DisplayName'" -ErrorAction SilentlyContinue - - if (-not $existingApp) { - # Create new application - $appParams = @{ - DisplayName = $DisplayName - Description = $DisplayName - SignInAudience = "AzureADMyOrg" - Web = @{ - RedirectUris = @("https://localhost") - } - } - - $newApp = New-EntraApplication @appParams - Write-Verbose "Created new application: $DisplayName" - - # Create service principal for the application - $spParams = @{ - AppId = $newApp.AppId - DisplayName = $DisplayName - } - - $newSp = New-EntraServicePrincipal @spParams - Write-Verbose "Created new service principal for application: $DisplayName" - - [PSCustomObject]@{ - ApplicationId = $newApp.Id - ApplicationDisplayName = $newApp.DisplayName - ServicePrincipalId = $newSp.Id - ServicePrincipalDisplayName = $newSp.DisplayName - Status = 'Created' - } - } - else { - # Get existing service principal - $existingSp = Get-EntraServicePrincipal -Filter "appId eq '$($existingApp.AppId)'" -ErrorAction SilentlyContinue - - if (-not $existingSp) { - # Create service principal if it doesn't exist - $spParams = @{ - AppId = $existingApp.AppId - DisplayName = $DisplayName - } - - $newSp = New-EntraServicePrincipal @spParams - Write-Verbose "Created new service principal for existing application: $DisplayName" - } - else { - $newSp = $existingSp - Write-Verbose "Service principal already exists for application: $DisplayName" - } - - [PSCustomObject]@{ - ApplicationId = $existingApp.Id - ApplicationDisplayName = $existingApp.DisplayName - ServicePrincipalId = $newSp.Id - ServicePrincipalDisplayName = $newSp.DisplayName - Status = 'Exists' - } - } - } - catch { - Write-Error "Error in Get-OrCreateEntraApplication: $_" - return $null - } - } - - function New-AppRoleIfNotExists($ApplicationId, $RoleDisplayName) { - - try { - # Get existing application - $application = Get-EntraApplication -ApplicationId $ApplicationId -ErrorAction Stop - if (-not $application) { - Write-Error "Application not found with ID: $ApplicationId" - return $null - } - - $AllowedMemberTypes = "User" - - # Create new app role - $appRole = @{ - AllowedMemberTypes = $AllowedMemberTypes - Description = $RoleDisplayName - DisplayName = $RoleDisplayName - Id = [guid]::NewGuid() - IsEnabled = $true - Value = $RoleValue ?? $RoleDisplayName - } - - # Get existing roles and add new role - $existingRoles = $application.AppRoles ?? @() - - # Check if role already exists - if ($existingRoles.Where({ $_.DisplayName -eq $RoleDisplayName })) { - Write-Warning "Role '$RoleDisplayName' already exists in application" - return $null - } - - $updatedRoles = $existingRoles + $appRole - - # Update application with new role - $params = @{ - ApplicationId = $ApplicationId - AppRoles = $updatedRoles - Tags = @("WindowsAzureActiveDirectoryIntegratedApp") - } - - $updatedApp = Set-EntraApplication @params - - Write-Verbose "Created new role '$RoleDisplayName' in application '$($application.DisplayName)'" - - # Return the newly created role - return [PSCustomObject]@{ - ApplicationId = $ApplicationId - RoleId = $appRole.Id - DisplayName = $RoleDisplayName - Description = $RoleDescription - Value = $appRole.Value - IsEnabled = $true - } - } - catch { - Write-Error "Failed to create app role: $_" - return $null - } - } - - function Assign-AppServicePrincipalRoleAssignmentIfNotExists($ServicePrincipalId, $UserId, $ApplicationName, $RoleDisplayName) { - - try { - # Check if assignment exists - - $servicePrincipalObject = Get-EntraServicePrincipal -ServicePrincipalId $ServicePrincipalId - $appRoleId = $servicePrincipalObject.AppRoles | Where-Object { $_.displayName -eq $RoleDisplayName } | Select-Object -Property Id - - $existingAssignment = Get-EntraServicePrincipalAppRoleAssignedTo -ServicePrincipalId $ServicePrincipalId | Where-Object { $_.AppRoleId -eq $appRoleId } -ErrorAction SilentlyContinue - - if ($existingAssignment) { - Write-Verbose "Role assignment already exists for user '$ApplicationName' with role '$RoleDisplayName'" - - return [PSCustomObject]@{ - ServicePrincipalId = $ServicePrincipalId - PrincipalId = $UserId - AppRoleId = $appRoleId - Status = 'Exists' - } - } - - # Create new assignment - - $newAssignment = New-EntraServicePrincipalAppRoleAssignment -ServicePrincipalId $ServicePrincipalId -ResourceId $ServicePrincipalId -Id $appRoleId -PrincipalId $UserId - Write-Verbose "Created new role assignment for user '$UserId' - AppName: '$ApplicationName' with role '$RoleDisplayName'" - - return [PSCustomObject]@{ - ServicePrincipalId = $ServicePrincipalId - PrincipalId = $UserId - AppRoleId = $appRoleId - Status = 'Created' - } - } - catch { - Write-Error "Failed to create or verify role assignment: $_" - return $null - } - } - - } - -} - diff --git a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 new file mode 100644 index 0000000000..aec7a8fbbe --- /dev/null +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -0,0 +1,529 @@ +function Set-EntraAppRoleToApplicationUser { + [CmdletBinding(DefaultParameterSetName = 'Default')] + param ( + [Parameter(Mandatory = $true, + HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic'", + ParameterSetName = 'Default')] + [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] + [ValidateSet("DatabaseorDirectory", "SAPCloudIdentity", "Generic")] + [string]$DataSource, + + [Parameter(Mandatory = $true, + HelpMessage = "Path to the input file containing users, e.g., C:\temp\users.csv", + ParameterSetName = 'Default')] + [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] + [ValidateNotNullOrEmpty()] + [ValidateScript({ Test-Path $_ })] + [string]$FileName, + + [Parameter(Mandatory = $true, + HelpMessage = "Name of the application (Service Principal) to assign roles for", + ParameterSetName = 'Default')] + [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] + [ValidateNotNullOrEmpty()] + [string]$ApplicationName, + + [Parameter(Mandatory = $true, + ParameterSetName = 'ExportResults', + HelpMessage = "Switch to enable export of results into a CSV file")] + [switch]$Export, + + [Parameter(Mandatory = $false, + ParameterSetName = 'ExportResults', + HelpMessage = "Path for the export file e.g. EntraAppRoleAssignments_yyyyMMdd.csv . If not specified, uses current location")] + [string]$ExportFileName = (Join-Path (Get-Location) "EntraAppRoleAssignments_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv") + ) + + process { + + + # Custom Verbose Function to Avoid Overriding Built-in Write-ColoredVerbose + function Write-ColoredVerbose { + param ( + [Parameter(Mandatory = $true)] + [string]$Message, + + [ValidateSet("Green", "Yellow", "Red", "Cyan", "Magenta")] + [string]$Color = "Cyan" + ) + + if ($VerbosePreference -eq "Continue") { + Write-Host "VERBOSE: $Message" -ForegroundColor $Color + } + } + + function SanitizeInputParameter { + param ([string]$Value) + + try { + if ([string]::IsNullOrWhiteSpace($Value)) { + Write-Warning "Input is empty or null." + return $null + } + + $cleanValue = $Value -replace "'", "''" + $cleanValue = $cleanValue -replace '\s+', ' ' # Replace multiple spaces with a single space + $cleanValue = $cleanValue.Trim().ToLower() # Trim and convert to lowercase + + if ([string]::IsNullOrWhiteSpace($cleanValue)) { + Write-Warning "Input became empty after sanitization." + return $null + } + + return $cleanValue + } + catch { + Write-Error "Error cleaning value: $_" + return $null + } + } + + function CreateUserIfNotExistsNew { + param ( + [string]$UserPrincipalName, + [string]$DisplayName, + [string]$MailNickname + ) + + Write-ColoredVerbose -Message "User details: DisplayName $DisplayName | UserPrincipalName: $UserPrincipalName | MailNickname: $MailNickname" -Color "Cyan" + + try { + $existingUser = Get-EntraUser -Filter "userPrincipalName eq '$UserPrincipalName'" -ErrorAction SilentlyContinue + if ($existingUser) { + Write-ColoredVerbose -Message "User $UserPrincipalName exists." -Color "Green" + return [PSCustomObject]@{ + Id = $existingUser.Id + UserPrincipalName = $existingUser.UserPrincipalName + DisplayName = $existingUser.DisplayName + MailNickname = $existingUser.MailNickname + Status = 'Exists' + } + } + + $passwordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile + $passwordProfile.EnforceChangePasswordPolicy = $true + $passwordProfile.Password = -join (((48..90) + (96..122)) * 16 | Get-Random -Count 16 | % { [char]$_ }) + $userParams = @{ + DisplayName = $DisplayName + PasswordProfile = $passwordProfile + UserPrincipalName = $UserPrincipalName + AccountEnabled = $false + MailNickName = $MailNickname + } + + $newUser = New-EntraUser @userParams + Write-ColoredVerbose -Message "Created new user: $UserPrincipalName" -Color "Green" + + return [PSCustomObject]@{ + Id = $newUser.Id + UserPrincipalName = $newUser.UserPrincipalName + DisplayName = $newUser.DisplayName + MailNickname = $newUser.MailNickname + Status = 'Created' + } + } + catch { + Write-Error "Failed to create or verify user $($UserPrincipalName) $_" + return $null + } + } + + function CreateUserIfNotExists { + param ( + [string]$UserPrincipalName, + [string]$DisplayName, + [string]$MailNickname + ) + + Write-ColoredVerbose -Message "User details: DisplayName $DisplayName | UserPrincipalName: $UserPrincipalName | MailNickname: $MailNickname" -Color "Cyan" + + try { + $existingUser = Get-EntraUser -Filter "userPrincipalName eq '$($UserPrincipalName)'" -ErrorAction SilentlyContinue + if ($existingUser) { + Write-ColoredVerbose -Message "User $UserPrincipalName exists." -Color "Green" + return $existingUser + } + + $passwordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile + $passwordProfile.EnforceChangePasswordPolicy = $true + $passwordProfile.Password = -join (((48..90) + (96..122)) * 16 | Get-Random -Count 16 | % { [char]$_ }) + $userParams = @{ + DisplayName = $DisplayName + PasswordProfile = $passwordProfile + UserPrincipalName = $UserPrincipalName + AccountEnabled = $false + MailNickName = $MailNickname + } + + $newUser = New-EntraUser @userParams + + Write-ColoredVerbose -Message "Created new user: $UserPrincipalName" -Color "Green" + return $newUser + } + catch { + Write-Error "Failed to create or verify user $($UserPrincipalName) : $_" + return $null + } + } + + function CreateApplicationIfNotExists { + param ([string]$DisplayName) + + try { + # Check if application exists + + $existingApp = Get-EntraApplication -Filter "displayName eq '$DisplayName'" -ErrorAction SilentlyContinue + + if (-not $existingApp) { + # Create new application + $appParams = @{ + DisplayName = $DisplayName + SignInAudience = "AzureADMyOrg" + Web = @{ + RedirectUris = @("https://localhost") + } + } + + $newApp = New-EntraApplication @appParams + Write-ColoredVerbose "Created new application: $DisplayName" + + # Create service principal for the application + $spParams = @{ + AppId = $newApp.AppId + DisplayName = $DisplayName + } + + $newSp = New-EntraServicePrincipal @spParams + Write-ColoredVerbose "Created new service principal for application: $DisplayName" + + [PSCustomObject]@{ + ApplicationId = $newApp.Id + ApplicationDisplayName = $newApp.DisplayName + ServicePrincipalId = $newSp.Id + ServicePrincipalDisplayName = $newSp.DisplayName + AppId = $newApp.AppId + Status = 'Created' + } + } + else { + # Get existing service principal + $existingSp = Get-EntraServicePrincipal -Filter "appId eq '$($existingApp.AppId)'" -ErrorAction SilentlyContinue + + if (-not $existingSp) { + # Create service principal if it doesn't exist + $spParams = @{ + AppId = $existingApp.AppId + DisplayName = $DisplayName + } + + $newSp = New-EntraServicePrincipal @spParams + Write-ColoredVerbose "Created new service principal for existing application: $DisplayName" + } + else { + $newSp = $existingSp + Write-ColoredVerbose "Service principal already exists for application: $DisplayName" + } + + [PSCustomObject]@{ + ApplicationId = $existingApp.Id + ApplicationDisplayName = $existingApp.DisplayName + ServicePrincipalId = $newSp.Id + ServicePrincipalDisplayName = $newSp.DisplayName + AppId = $existingApp.AppId + Status = 'Exists' + } + } + } + catch { + Write-Error "Error in CreateApplicationIfNotExists: $_" + return $null + } + } + + function AssignAppServicePrincipalRoleAssignmentIfNotExists { + + param ( + [string]$ServicePrincipalId, + [string]$UserId, + [string]$ApplicationName, + [string]$RoleDisplayName + ) + + try { + # Check if assignment exists + + $servicePrincipalObject = Get-EntraServicePrincipal -ServicePrincipalId $ServicePrincipalId + $appRoleId = ($servicePrincipalObject.AppRoles | Where-Object { $_.displayName -eq $RoleDisplayName }).Id + + $existingAssignment = Get-EntraServicePrincipalAppRoleAssignedTo -ServicePrincipalId $servicePrincipalObject.Id | Where-Object { $_.AppRoleId -eq $appRoleId } -ErrorAction SilentlyContinue + + if ($existingAssignment) { + Write-ColoredVerbose "Role assignment already exists for user '$ApplicationName' with role '$RoleDisplayName'" -Color "Yellow" + + return [PSCustomObject]@{ + ServicePrincipalId = $ServicePrincipalId + PrincipalId = $UserId + AppRoleId = $appRoleId + AssignmentId = $existingAssignment.Id + Status = 'Exists' + CreatedDateTime = $existingAssignment.CreatedDateTime #?? (Get-Date).ToUniversalTime().ToString("o") + } + } + + # Create new assignment + $newAssignment = New-EntraServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipalObject.Id -ResourceId $servicePrincipalObject.Id -Id $appRoleId -PrincipalId $UserId + Write-ColoredVerbose "Created new role assignment for user '$UserId' - AppName: '$ApplicationName' with role '$RoleDisplayName'" -Color "Green" + + return [PSCustomObject]@{ + ServicePrincipalId = $ServicePrincipalId + PrincipalId = $UserId + AppRoleId = $appRoleId + AssignmentId = $newAssignment.Id + Status = 'Created' + CreatedDateTime = $newAssignment.CreatedDateTime #(Get-Date).ToUniversalTime().ToString("o") # ISO 8601 format + } + } + catch { + Write-Error "Failed to create or verify role assignment: $_)" + return $null + } + } + + function NewAppRoleIfNotExists { + param ( + [Parameter(Mandatory = $true)] + [string[]]$UniqueRoles, + + [Parameter(Mandatory = $true)] + [string]$ApplicationId + ) + + try { + # Get existing application + $application = Get-MgApplication -ApplicationId $ApplicationId -ErrorAction Stop + if (-not $application) { + Write-Error "Application not found with ID: $ApplicationId" + return $null + } + + # Ensure the existing AppRoles are properly formatted + $existingRoles = $application.AppRoles ?? @() + $appRolesList = New-Object 'System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphAppRole]' + + foreach ($role in $existingRoles) { + $appRolesList.Add($role) + } + + $allowedMemberTypes = @("User") # Define allowed member types + $createdRoles = [System.Collections.ArrayList]::new() + + foreach ($roleName in $UniqueRoles) { + # Check if the role already exists + if ($existingRoles | Where-Object { $_.DisplayName -eq $roleName }) { + Write-ColoredVerbose "Role '$roleName' already exists in application" -Color "Yellow" + continue + } + + # Create new AppRole object + $appRole = [Microsoft.Graph.PowerShell.Models.MicrosoftGraphAppRole]@{ + AllowedMemberTypes = $allowedMemberTypes + Description = $roleName + DisplayName = $roleName + Id = [Guid]::NewGuid() + IsEnabled = $true + Value = $roleName + } + + # Add to the typed list + $appRolesList.Add($appRole) + [void]$createdRoles.Add($appRole) + Write-ColoredVerbose "Created new role definition for '$roleName'" -Color "Green" + } + + if ($createdRoles.Count -gt 0) { + # Update application with the new typed list + $params = @{ + ApplicationId = $ApplicationId + AppRoles = $appRolesList + Tags = @("WindowsAzureActiveDirectoryIntegratedApp") + } + + Update-MgApplication @params + Write-ColoredVerbose "Updated application with $($createdRoles.Count) new roles" -Color "Green" + + return $createdRoles | ForEach-Object { + [PSCustomObject]@{ + ApplicationId = $ApplicationId + RoleId = $_.Id + DisplayName = $_.DisplayName + Description = $_.Description + Value = $_.Value + IsEnabled = $true + } + } + } + + Write-ColoredVerbose "No new roles needed to be created" -Color "Yellow" + return $null + } + catch { + Write-Error "Failed to create app roles: $_" + return $null + } + } + + function StartOrchestration { + + try { + # Import users from the CSV file + Write-ColoredVerbose "Importing users from file: $FileName" -Color "Cyan" + $users = Import-Csv -Path $FileName + Write-ColoredVerbose "Imported : $($users.Count) users" -Color "Green" + if (-not $users) { + Write-Error "No users found in the provided file: $FileName" + return + } + + # Define the property name for user lookup based on data source + Write-ColoredVerbose "Using: $DataSource for pattern matching" -Color "Cyan" + $sourceMatchPropertyName = switch ($DataSource) { + "DatabaseorDirectory" { "email" } + "SAPCloudIdentity" { "userName" } + "Generic" { "userPrincipalName" } + } + Write-ColoredVerbose "Column used for lookup in Entra ID : $sourceMatchPropertyName." -Color "Green" + + # Get or create the application and service principal once + Write-ColoredVerbose -Message "Checking if application exists for: $ApplicationName" -Color "Cyan" + $application = CreateApplicationIfNotExists -DisplayName $ApplicationName + if (-not $application) { + Write-Error "Failed to retrieve or create application: $ApplicationName" + return + } + Write-ColoredVerbose "Application $ApplicationName status: $($application.Status) | ApplicationId : $($application.ApplicationId) | AppId : $($application.AppId) | ServicePrincipalId : $($application.ServicePrincipalId)." -Color "Green" + + $uniqueRoles = @() + + # Extract unique roles + $users | ForEach-Object { + $role = SanitizeInputParameter -Value $_.Role + if ($role -and $role -notin $uniqueRoles) { + $uniqueRoles += $role + } + } + + Write-ColoredVerbose "Found $($uniqueRoles.Count) unique roles: $($uniqueRoles -join ', ')" -Color "Green" + # Create new roles if they do not exist + + if ($uniqueRoles.Count -gt 0) { + Write-ColoredVerbose "Creating required roles in application..." -Color "Cyan" + $createdRoles = NewAppRoleIfNotExists -UniqueRoles $uniqueRoles -ApplicationId $application.ApplicationId + if ($createdRoles) { + Write-ColoredVerbose "Successfully created $($createdRoles.Count) new roles" -Color "Green" + } + } + else { + Write-ColoredVerbose "No new roles needed to be created" -Color "Yellow" + } + # Process users in bulk + + Write-ColoredVerbose "Processing users details..." -Color "Cyan" + + $assignmentResults = [System.Collections.ArrayList]::new() + + foreach ($user in $users) { + + #$cleanUserPrincipalName = SanitizeInputParameter($user.$sourceMatchPropertyName) + $cleanUserPrincipalName = SanitizeInputParameter -Value $user.$sourceMatchPropertyName + Write-ColoredVerbose "UPN : $($cleanUserPrincipalName)" -Color "Green" + + if (-not $cleanUserPrincipalName) { + Write-Warning "Skipping user due to invalid userPrincipalName: $($user.$sourceMatchPropertyName)" + continue + } + + $cleanDisplayName = SanitizeInputParameter -Value $user.displayName + Write-ColoredVerbose "DisplayName : $($cleanDisplayName)" -Color "Green" + + if (-not $cleanDisplayName) { + Write-Warning "Skipping user due to invalid displayName: $($user.DisplayName)" + continue + } + $cleanMailNickname = SanitizeInputParameter -Value $user.mailNickname + Write-ColoredVerbose "Mail nickname : $($cleanMailNickname)" -Color "Green" + + if (-not $cleanMailNickname) { + Write-Warning "Skipping user due to invalid mailNickname: $($user.MailNickname)" + continue + } + + # Get the user's role + $userRole = SanitizeInputParameter -Value $user.Role + Write-ColoredVerbose "Role : $($userRole)" -Color "Green" + if (-not $userRole) { + Write-Warning "Skipping user due to invalid Role: $($user.Role)" + continue + } + + + # Create user if they do not exist + Write-ColoredVerbose "Assigning roles to user $($cleanUserPrincipalName) " + $userInfo = CreateUserIfNotExistsNew -UserPrincipalName $cleanUserPrincipalName -DisplayName $cleanDisplayName -MailNickname $cleanMailNickname + + if (-not $userInfo) { continue } + + # Assign roles to the user (Placeholder for role assignment logic) + Write-ColoredVerbose "Start app role assignment with params: ServicePrincipalId - $($application.ServicePrincipalId) | UserId - $($userInfo.Id) | AppName - $($ApplicationName) | Role - $($userRole) " -Color "Cyan" + $assignment = AssignAppServicePrincipalRoleAssignmentIfNotExists -ServicePrincipalId $application.ServicePrincipalId -UserId $userInfo.Id -ApplicationName $ApplicationName -RoleDisplayName $userRole + if (-not $assignment) { continue } + Write-ColoredVerbose "Assigning roles to user $($userInfo.UserPrincipalName) in application $ApplicationName" + + if ($assignment) { + [void]$assignmentResults.Add([PSCustomObject]@{ + UserPrincipalName = $cleanUserPrincipalName + DisplayName = $cleanDisplayName + UserId = $userInfo.Id + Role = $userRole + ApplicationName = $ApplicationName + ApplicationStatus = $application.Status + ServicePrincipalId = $application.ServicePrincipalId + UserCreationStatus = $userInfo.Status + RoleAssignmentStatus = $assignment.Status + AssignmentId = $assignment.AssignmentId + AppRoleId = $assignment.AppRoleId + PrincipalType = "User" # Based on the AllowedMemberTypes in role creation + RoleAssignmentCreatedDateTime = $assignment.CreatedDateTime + ResourceId = $application.ServicePrincipalId # Same as ServicePrincipalId in this context + ProcessedTimestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + }) + } + } + + # Export results if using ExportResults parameter set + if ($PSCmdlet.ParameterSetName -eq 'ExportResults' -and $assignmentResults.Count -gt 0) { + try { + Write-ColoredVerbose "Exporting results to: $ExportFileName" -Color "Cyan" + $assignmentResults | Export-Csv -Path $ExportFileName -NoTypeInformation -Force + Write-ColoredVerbose "Successfully exported $($assignmentResults.Count) assignments" -Color "Green" + } + catch { + Write-Error "Failed to export results: $_" + } + } + + return $assignmentResults + } + catch { + Write-Error "Error in StartOrchestration: $_)" + } + } + + # Debugging output + Write-ColoredVerbose -Message "Starting orchestration with params: AppName - $ApplicationName | FileName - $FileName | DataSource - $DataSource" -Color "Magenta" + # Start orchestration + StartOrchestration + + } + +} + From e6bcae22c394c62f8060f632cb5873b4545dda55 Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Fri, 28 Feb 2025 13:44:44 +0300 Subject: [PATCH 3/9] Add docs and bumping up Acrolinx score --- .../Set-EntraAppRoleToApplicationUser.ps1 | 96 ++------ .../Set-EntraAppRoleToApplicationUser.md | 223 ++++++++++++++++++ 2 files changed, 243 insertions(+), 76 deletions(-) create mode 100644 module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md diff --git a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 index aec7a8fbbe..51449f0e89 100644 --- a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -1,8 +1,12 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. +# Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ function Set-EntraAppRoleToApplicationUser { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, - HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic'", + HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic' which determines the column attribute mapping.", ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] [ValidateSet("DatabaseorDirectory", "SAPCloudIdentity", "Generic")] @@ -14,7 +18,7 @@ function Set-EntraAppRoleToApplicationUser { [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path $_ })] - [string]$FileName, + [System.IO.FileInfo]$FileName, [Parameter(Mandatory = $true, HelpMessage = "Name of the application (Service Principal) to assign roles for", @@ -27,11 +31,10 @@ function Set-EntraAppRoleToApplicationUser { ParameterSetName = 'ExportResults', HelpMessage = "Switch to enable export of results into a CSV file")] [switch]$Export, - - [Parameter(Mandatory = $false, - ParameterSetName = 'ExportResults', - HelpMessage = "Path for the export file e.g. EntraAppRoleAssignments_yyyyMMdd.csv . If not specified, uses current location")] - [string]$ExportFileName = (Join-Path (Get-Location) "EntraAppRoleAssignments_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv") + + [Parameter(Mandatory = $false, ParameterSetName = 'ExportResults', + HelpMessage = "Path for the export file. Defaults to current directory.")] + [System.IO.FileInfo]$ExportFileName = (Join-Path (Get-Location) "EntraAppRoleAssignments_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv") ) process { @@ -52,33 +55,13 @@ function Set-EntraAppRoleToApplicationUser { } } - function SanitizeInputParameter { + function SanitizeInput { param ([string]$Value) - - try { - if ([string]::IsNullOrWhiteSpace($Value)) { - Write-Warning "Input is empty or null." - return $null - } - - $cleanValue = $Value -replace "'", "''" - $cleanValue = $cleanValue -replace '\s+', ' ' # Replace multiple spaces with a single space - $cleanValue = $cleanValue.Trim().ToLower() # Trim and convert to lowercase - - if ([string]::IsNullOrWhiteSpace($cleanValue)) { - Write-Warning "Input became empty after sanitization." - return $null - } - - return $cleanValue - } - catch { - Write-Error "Error cleaning value: $_" - return $null - } + if ([string]::IsNullOrWhiteSpace($Value)) { return $null } + return $Value -replace "'", "''" -replace '\s+', ' ' -replace "`n|`r", "" | ForEach-Object { $_.Trim().ToLower() } } - function CreateUserIfNotExistsNew { + function CreateUserIfNotExists { param ( [string]$UserPrincipalName, [string]$DisplayName, @@ -128,44 +111,6 @@ function Set-EntraAppRoleToApplicationUser { } } - function CreateUserIfNotExists { - param ( - [string]$UserPrincipalName, - [string]$DisplayName, - [string]$MailNickname - ) - - Write-ColoredVerbose -Message "User details: DisplayName $DisplayName | UserPrincipalName: $UserPrincipalName | MailNickname: $MailNickname" -Color "Cyan" - - try { - $existingUser = Get-EntraUser -Filter "userPrincipalName eq '$($UserPrincipalName)'" -ErrorAction SilentlyContinue - if ($existingUser) { - Write-ColoredVerbose -Message "User $UserPrincipalName exists." -Color "Green" - return $existingUser - } - - $passwordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile - $passwordProfile.EnforceChangePasswordPolicy = $true - $passwordProfile.Password = -join (((48..90) + (96..122)) * 16 | Get-Random -Count 16 | % { [char]$_ }) - $userParams = @{ - DisplayName = $DisplayName - PasswordProfile = $passwordProfile - UserPrincipalName = $UserPrincipalName - AccountEnabled = $false - MailNickName = $MailNickname - } - - $newUser = New-EntraUser @userParams - - Write-ColoredVerbose -Message "Created new user: $UserPrincipalName" -Color "Green" - return $newUser - } - catch { - Write-Error "Failed to create or verify user $($UserPrincipalName) : $_" - return $null - } - } - function CreateApplicationIfNotExists { param ([string]$DisplayName) @@ -406,7 +351,7 @@ function Set-EntraAppRoleToApplicationUser { # Extract unique roles $users | ForEach-Object { - $role = SanitizeInputParameter -Value $_.Role + $role = SanitizeInput -Value $_.Role if ($role -and $role -notin $uniqueRoles) { $uniqueRoles += $role } @@ -433,8 +378,7 @@ function Set-EntraAppRoleToApplicationUser { foreach ($user in $users) { - #$cleanUserPrincipalName = SanitizeInputParameter($user.$sourceMatchPropertyName) - $cleanUserPrincipalName = SanitizeInputParameter -Value $user.$sourceMatchPropertyName + $cleanUserPrincipalName = SanitizeInput -Value $user.$sourceMatchPropertyName Write-ColoredVerbose "UPN : $($cleanUserPrincipalName)" -Color "Green" if (-not $cleanUserPrincipalName) { @@ -442,14 +386,14 @@ function Set-EntraAppRoleToApplicationUser { continue } - $cleanDisplayName = SanitizeInputParameter -Value $user.displayName + $cleanDisplayName = SanitizeInput -Value $user.displayName Write-ColoredVerbose "DisplayName : $($cleanDisplayName)" -Color "Green" if (-not $cleanDisplayName) { Write-Warning "Skipping user due to invalid displayName: $($user.DisplayName)" continue } - $cleanMailNickname = SanitizeInputParameter -Value $user.mailNickname + $cleanMailNickname = SanitizeInput -Value $user.mailNickname Write-ColoredVerbose "Mail nickname : $($cleanMailNickname)" -Color "Green" if (-not $cleanMailNickname) { @@ -458,7 +402,7 @@ function Set-EntraAppRoleToApplicationUser { } # Get the user's role - $userRole = SanitizeInputParameter -Value $user.Role + $userRole = SanitizeInput -Value $user.Role Write-ColoredVerbose "Role : $($userRole)" -Color "Green" if (-not $userRole) { Write-Warning "Skipping user due to invalid Role: $($user.Role)" @@ -468,7 +412,7 @@ function Set-EntraAppRoleToApplicationUser { # Create user if they do not exist Write-ColoredVerbose "Assigning roles to user $($cleanUserPrincipalName) " - $userInfo = CreateUserIfNotExistsNew -UserPrincipalName $cleanUserPrincipalName -DisplayName $cleanDisplayName -MailNickname $cleanMailNickname + $userInfo = CreateUserIfNotExists -UserPrincipalName $cleanUserPrincipalName -DisplayName $cleanDisplayName -MailNickname $cleanMailNickname if (-not $userInfo) { continue } diff --git a/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md b/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md new file mode 100644 index 0000000000..022e8c860f --- /dev/null +++ b/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md @@ -0,0 +1,223 @@ +--- +title: Set-EntraAppRoleToApplicationUser +description: This article provides details on the Set-EntraAppRoleToApplicationUser command. + +ms.topic: reference +ms.date: 02/28/2025 +ms.author: eunicewaweru +ms.reviewer: stevemutungi +manager: CelesteDG +author: msewaweru + +external help file: Microsoft.Entra.Governance-Help.xml +Module Name: Microsoft.Entra +online version: https://learn.microsoft.com/powershell/module/Microsoft.Entra/Set-EntraAppRoleToApplicationUser + +schema: 2.0.0 +--- + +# Set-EntraAppRoleToApplicationUser + +## Synopsis + +Add existing application users to Microsoft Entra ID and assign them roles. + +## Syntax + +### Default + +```powershell +Set-EntraAppRoleToApplicationUser + -DataSource + -FileName + -ApplicationName + [] +``` + +### ExportResults + +```powershell +Set-EntraAppRoleToApplicationUser + -DataSource + -FileName + -ApplicationName + -Export + -ExportFileName + [] +``` + +## Description + +The `Set-EntraAppRoleToApplicationUser` command adds existing users (for example, from a Helpdesk or billing application) to Microsoft Entra ID and assigns them app roles like Admin, Audit, or Reports. This enables the application unlock Microsoft Entra ID Governance features like access reviews. + +This feature requires a Microsoft Entra ID Governance or Microsoft Entra Suite license, see [Microsoft Entra ID Governance licensing fundamentals](https://learn.microsoft.com/entra/id-governance/licensing-fundamentals). + +In delegated scenarios, the signed-in user must have either a supported Microsoft Entra role or a custom role with the necessary permissions. The minimum roles required for this operation are: + +- User Administrator (create users) +- Application Administrator +- Identity Governance Administrator (manage application role assignments) + +## Examples + +### Example 1: Assign application users to app role assignments + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All', 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'EntitlementManagement.ReadWrite.All' +Set-EntraAppRoleToApplicationUser -DataSource "Generic" -FileName "C:\temp\users.csv" -ApplicationName "TestApp" +``` + +This example assigns users to app roles. It creates missing users and app roles. If a role assignment doesn't exist, it's created; otherwise, it's skipped. + +- `-DataSource` parameter specifies the source of the data, for example, SAP Identity, database, or directory. The value determines the attribute matching. For example, For SAP Cloud Identity Services, the default mapping is `userName` (SAP SCIM) to `userPrincipalName` (Microsoft Entra ID). For databases or directories, the `Email` column value might match the `userPrincipalName` in Microsoft Entra ID. +- `-FileName` parameter specifies the path to the input file containing users, for example, C:\temp\users.csv. +- `-ApplicationName` parameter specifies the application name in Microsoft Entra ID. + +### Example 2: Assign application users to app role assignments with verbose mode + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All', 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'EntitlementManagement.ReadWrite.All' +Set-EntraAppRoleToApplicationUser -DataSource "SAPCloudIdentity" -FileName "C:\temp\users-exported-from-sap.csv" -ApplicationName "TestApp" -Verbose +``` + +This example assigns users to app roles. It creates missing users and app roles. If a role assignment doesn't exist, it's created; otherwise, it's skipped. + +- `-DataSource` parameter specifies the source of the data, for example, SAP Identity, database, or directory. The value determines the attribute matching. For example, For SAP Cloud Identity Services, the default mapping is `userName` (SAP SCIM) to `userPrincipalName` (Microsoft Entra ID). For databases or directories, the `Email` column value might match the `userPrincipalName` in Microsoft Entra ID. +- `-FileName` parameter specifies the path to the input file containing users, for example, C:\temp\users.csv. +- `-ApplicationName` parameter specifies the application name in Microsoft Entra ID. +- `-Verbose` common parameter outputs the execution steps during processing. + +### Example 3: Assign application users to app roles and export to a default location + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All', 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'EntitlementManagement.ReadWrite.All' +Set-EntraAppRoleToApplicationUser -DataSource "Generic" -FileName "C:\temp\users.csv" -ApplicationName "TestApp" -Export -Verbose +``` + +This example assigns users to app roles. It creates missing users and app roles. If a role assignment doesn't exist, it's created; otherwise, it's skipped. + +- `-DataSource` parameter specifies the source of the data, for example, SAP Identity, database, or directory. The value determines the attribute matching. For example, For SAP Cloud Identity Services, the default mapping is `userName` (SAP SCIM) to `userPrincipalName` (Microsoft Entra ID). For databases or directories, the `Email` column value might match the `userPrincipalName` in Microsoft Entra ID. +- `-FileName` parameter specifies the path to the input file containing users, for example, C:\temp\users.csv. +- `-ApplicationName` parameter specifies the application name in Microsoft Entra ID. +- `-Export` switch parameter enables export of results into a CSV file. If `ExportFileName` parameter isn't provided, results are exported in the current location. +- `-Verbose` common parameter outputs the execution steps during processing. + +### Example 4: Assign application users to app roles and export to a specified location + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All', 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'EntitlementManagement.ReadWrite.All' +Set-EntraAppRoleToApplicationUser -DataSource "Generic" -FileName "C:\temp\users.csv" -ApplicationName "TestApp" -Export -ExportFileName "C:\temp\EntraAppRoleAssignments_yyyyMMdd.csv" -Verbose +``` + +This example assigns users to app roles. It creates missing users and app roles. If a role assignment doesn't exist, it's created; otherwise, it's skipped. + +- `-DataSource` parameter specifies the source of the data, for example, SAP Identity, database, or directory. The value determines the attribute matching. For example, For SAP Cloud Identity Services, the default mapping is `userName` (SAP SCIM) to `userPrincipalName` (Microsoft Entra ID). For databases or directories, the `Email` column value might match the `userPrincipalName` in Microsoft Entra ID. +- `-FileName` parameter specifies the path to the input file containing users, for example, C:\temp\users.csv. +- `-ApplicationName` parameter specifies the application name in Microsoft Entra ID. +- `-Export` switch parameter enables export of results into a CSV file. If `ExportFileName` parameter isn't provided, results are exported in the current location. +- `-ExportFileName` parameter specifies a specific filename and location to export results. +- `-Verbose` common parameter outputs the execution steps during processing. + +## Parameters + +### -DataSource + +Specifies the source of the data, for example, SAP Identity, database, or directory. The value determines the attribute matching. For example, For SAP Cloud Identity Services, the default mapping is `userName` (SAP SCIM) to `userPrincipalName` (Microsoft Entra ID). For databases or directories, the `Email` column value might match the `userPrincipalName` in Microsoft Entra ID. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: +Accept wildcard characters: False +``` + +### -FileName + +Specifies the path to the input file containing users, for example, C:\temp\users.csv. + +```yaml +Type: System.IO.FileInfo +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ApplicationName + +Specifies the application name in Microsoft Entra ID. + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Export + +Enables export of results into a CSV file. If `ExportFileName` parameter isn't provided, results are exported in the current location. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (ExportResults) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ExportFileName + +Specifies a specific filename and location to export results. + +```yaml +Type: System.IO.FileInfo +Parameter Sets: (ExportResults) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: `-Debug`, `-ErrorAction`, `-ErrorVariable`, `-InformationAction`, `-InformationVariable`, `-OutVariable`, `-OutBuffer`, `-PipelineVariable`, `-Verbose`, `-WarningAction`, and `-WarningVariable`. For more information, see [about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## Inputs + +### System.String + +## Outputs + +### System.Object + +## Notes + +[Govern an application's existing users](https://learn.microsoft.com/entra/id-governance/identity-governance-applications-existing-users) + +## Related Links + +[Get-EntraServicePrincipalAppRoleAssignedTo](Get-EntraServicePrincipalAppRoleAssignedTo.md) + +[New-EntraServicePrincipalAppRoleAssignment](New-EntraServicePrincipalAppRoleAssignment.md) From 38f1e70fef77cab6294751ecf7d225b5fcbd5a37 Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Fri, 28 Feb 2025 13:50:05 +0300 Subject: [PATCH 4/9] File path fixes --- .../Governance/Set-EntraAppRoleToApplicationUser.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md b/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md index 022e8c860f..d0da706f60 100644 --- a/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md +++ b/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md @@ -218,6 +218,6 @@ This cmdlet supports the common parameters: `-Debug`, `-ErrorAction`, `-ErrorVar ## Related Links -[Get-EntraServicePrincipalAppRoleAssignedTo](Get-EntraServicePrincipalAppRoleAssignedTo.md) +[Get-EntraServicePrincipalAppRoleAssignedTo](../Applications/Get-EntraServicePrincipalAppRoleAssignedTo.md) -[New-EntraServicePrincipalAppRoleAssignment](New-EntraServicePrincipalAppRoleAssignment.md) +[New-EntraServicePrincipalAppRoleAssignment](../Applications/New-EntraServicePrincipalAppRoleAssignment.md) From 17e316fa42f17470783eba4c1ccec8438bd43ad8 Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Mon, 3 Mar 2025 14:25:27 +0300 Subject: [PATCH 5/9] Adding support for WhatIf --- .../Set-EntraAppRoleToApplicationUser.ps1 | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 index 51449f0e89..0f54625fc4 100644 --- a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License in the project root for license information. # ------------------------------------------------------------------------------ function Set-EntraAppRoleToApplicationUser { - [CmdletBinding(DefaultParameterSetName = 'Default')] + [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic' which determines the column attribute mapping.", @@ -94,7 +94,9 @@ function Set-EntraAppRoleToApplicationUser { MailNickName = $MailNickname } - $newUser = New-EntraUser @userParams + if ($PSCmdlet.ShouldProcess("User '$UserPrincipalName'", "Create")) { + $newUser = New-EntraUser @userParams + } Write-ColoredVerbose -Message "Created new user: $UserPrincipalName" -Color "Green" return [PSCustomObject]@{ @@ -129,7 +131,10 @@ function Set-EntraAppRoleToApplicationUser { } } - $newApp = New-EntraApplication @appParams + if ($PSCmdlet.ShouldProcess("Application '$DisplayName'", "Create")) { + $newApp = New-EntraApplication @appParams + } + Write-ColoredVerbose "Created new application: $DisplayName" # Create service principal for the application @@ -138,7 +143,11 @@ function Set-EntraAppRoleToApplicationUser { DisplayName = $DisplayName } - $newSp = New-EntraServicePrincipal @spParams + + + if ($PSCmdlet.ShouldProcess("Service principal '$DisplayName'", "Create")) { + $newSp = New-EntraServicePrincipal @spParams + } Write-ColoredVerbose "Created new service principal for application: $DisplayName" [PSCustomObject]@{ @@ -161,7 +170,9 @@ function Set-EntraAppRoleToApplicationUser { DisplayName = $DisplayName } - $newSp = New-EntraServicePrincipal @spParams + if ($PSCmdlet.ShouldProcess("Service principal '$DisplayName'", "Create")) { + $newSp = New-EntraServicePrincipal @spParams + } Write-ColoredVerbose "Created new service principal for existing application: $DisplayName" } else { @@ -216,7 +227,10 @@ function Set-EntraAppRoleToApplicationUser { } # Create new assignment - $newAssignment = New-EntraServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipalObject.Id -ResourceId $servicePrincipalObject.Id -Id $appRoleId -PrincipalId $UserId + if ($PSCmdlet.ShouldProcess("Service Principal App Role assignment: AppRole - '$appRoleId' | UserId - '$UserId' | Service Principal - '$servicePrincipalObject.Id'", "Create")) { + $newAssignment = New-EntraServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipalObject.Id -ResourceId $servicePrincipalObject.Id -Id $appRoleId -PrincipalId $UserId + } + Write-ColoredVerbose "Created new role assignment for user '$UserId' - AppName: '$ApplicationName' with role '$RoleDisplayName'" -Color "Green" return [PSCustomObject]@{ @@ -282,6 +296,7 @@ function Set-EntraAppRoleToApplicationUser { # Add to the typed list $appRolesList.Add($appRole) [void]$createdRoles.Add($appRole) + Write-ColoredVerbose "Created new role definition for '$roleName'" -Color "Green" } @@ -293,7 +308,10 @@ function Set-EntraAppRoleToApplicationUser { Tags = @("WindowsAzureActiveDirectoryIntegratedApp") } - Update-MgApplication @params + + if ($PSCmdlet.ShouldProcess("Update application '$DisplayName' with AppRole list - '$appRolesList'", "Update")) { + Update-MgApplication @params + } Write-ColoredVerbose "Updated application with $($createdRoles.Count) new roles" -Color "Green" return $createdRoles | ForEach-Object { From c3a76fc52055cee604d40f3989d02b349b7fa87a Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Mon, 3 Mar 2025 16:16:27 +0300 Subject: [PATCH 6/9] Add WhatIf scenario when creating an app --- .../Set-EntraAppRoleToApplicationUser.ps1 | 84 +++++++++++-------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 index 0f54625fc4..f22963852d 100644 --- a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -115,42 +115,49 @@ function Set-EntraAppRoleToApplicationUser { function CreateApplicationIfNotExists { param ([string]$DisplayName) - + try { # Check if application exists - $existingApp = Get-EntraApplication -Filter "displayName eq '$DisplayName'" -ErrorAction SilentlyContinue - + if (-not $existingApp) { - # Create new application - $appParams = @{ - DisplayName = $DisplayName - SignInAudience = "AzureADMyOrg" - Web = @{ - RedirectUris = @("https://localhost") - } - } - if ($PSCmdlet.ShouldProcess("Application '$DisplayName'", "Create")) { + $appParams = @{ + DisplayName = $DisplayName + SignInAudience = "AzureADMyOrg" + Web = @{ RedirectUris = @("https://localhost") } + } $newApp = New-EntraApplication @appParams + Write-ColoredVerbose "Created new application: $DisplayName" } - - Write-ColoredVerbose "Created new application: $DisplayName" - - # Create service principal for the application - $spParams = @{ - AppId = $newApp.AppId - DisplayName = $DisplayName + else { + # Handle -WhatIf scenario by returning a mock object + $newApp = [PSCustomObject]@{ + Id = "WhatIf-AppId" + AppId = "WhatIf-AppId" + DisplayName = $DisplayName + } + Write-ColoredVerbose "WhatIf: Simulating creation of application: $DisplayName" } - - - + if ($PSCmdlet.ShouldProcess("Service principal '$DisplayName'", "Create")) { + $spParams = @{ + AppId = $newApp.AppId + DisplayName = $DisplayName + } $newSp = New-EntraServicePrincipal @spParams + Write-ColoredVerbose "Created new service principal for application: $DisplayName" } - Write-ColoredVerbose "Created new service principal for application: $DisplayName" - - [PSCustomObject]@{ + else { + # Handle -WhatIf scenario + $newSp = [PSCustomObject]@{ + Id = "WhatIf-ServicePrincipalId" + DisplayName = $DisplayName + } + Write-ColoredVerbose "WhatIf: Simulating creation of service principal for application: $DisplayName" + } + + return [PSCustomObject]@{ ApplicationId = $newApp.Id ApplicationDisplayName = $newApp.DisplayName ServicePrincipalId = $newSp.Id @@ -160,27 +167,31 @@ function Set-EntraAppRoleToApplicationUser { } } else { - # Get existing service principal $existingSp = Get-EntraServicePrincipal -Filter "appId eq '$($existingApp.AppId)'" -ErrorAction SilentlyContinue - + if (-not $existingSp) { - # Create service principal if it doesn't exist - $spParams = @{ - AppId = $existingApp.AppId - DisplayName = $DisplayName - } - if ($PSCmdlet.ShouldProcess("Service principal '$DisplayName'", "Create")) { + $spParams = @{ + AppId = $existingApp.AppId + DisplayName = $DisplayName + } $newSp = New-EntraServicePrincipal @spParams + Write-ColoredVerbose "Created new service principal for existing application: $DisplayName" + } + else { + $newSp = [PSCustomObject]@{ + Id = "WhatIf-ServicePrincipalId" + DisplayName = $DisplayName + } + Write-ColoredVerbose "WhatIf: Simulating creation of service principal for existing application: $DisplayName" } - Write-ColoredVerbose "Created new service principal for existing application: $DisplayName" } else { $newSp = $existingSp Write-ColoredVerbose "Service principal already exists for application: $DisplayName" } - - [PSCustomObject]@{ + + return [PSCustomObject]@{ ApplicationId = $existingApp.Id ApplicationDisplayName = $existingApp.DisplayName ServicePrincipalId = $newSp.Id @@ -195,6 +206,7 @@ function Set-EntraAppRoleToApplicationUser { return $null } } + function AssignAppServicePrincipalRoleAssignmentIfNotExists { From bc972296f86f1310ff0cf617bef0b8a41b2aefe8 Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Mon, 3 Mar 2025 19:38:40 +0300 Subject: [PATCH 7/9] Reverting WhatIf checks --- .../Set-EntraAppRoleToApplicationUser.ps1 | 104 +++++++----------- 1 file changed, 37 insertions(+), 67 deletions(-) diff --git a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 index f22963852d..51449f0e89 100644 --- a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -3,7 +3,7 @@ # Licensed under the MIT License. See License in the project root for license information. # ------------------------------------------------------------------------------ function Set-EntraAppRoleToApplicationUser { - [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'Default')] + [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true, HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic' which determines the column attribute mapping.", @@ -94,9 +94,7 @@ function Set-EntraAppRoleToApplicationUser { MailNickName = $MailNickname } - if ($PSCmdlet.ShouldProcess("User '$UserPrincipalName'", "Create")) { - $newUser = New-EntraUser @userParams - } + $newUser = New-EntraUser @userParams Write-ColoredVerbose -Message "Created new user: $UserPrincipalName" -Color "Green" return [PSCustomObject]@{ @@ -115,49 +113,35 @@ function Set-EntraAppRoleToApplicationUser { function CreateApplicationIfNotExists { param ([string]$DisplayName) - + try { # Check if application exists + $existingApp = Get-EntraApplication -Filter "displayName eq '$DisplayName'" -ErrorAction SilentlyContinue - + if (-not $existingApp) { - if ($PSCmdlet.ShouldProcess("Application '$DisplayName'", "Create")) { - $appParams = @{ - DisplayName = $DisplayName - SignInAudience = "AzureADMyOrg" - Web = @{ RedirectUris = @("https://localhost") } - } - $newApp = New-EntraApplication @appParams - Write-ColoredVerbose "Created new application: $DisplayName" - } - else { - # Handle -WhatIf scenario by returning a mock object - $newApp = [PSCustomObject]@{ - Id = "WhatIf-AppId" - AppId = "WhatIf-AppId" - DisplayName = $DisplayName + # Create new application + $appParams = @{ + DisplayName = $DisplayName + SignInAudience = "AzureADMyOrg" + Web = @{ + RedirectUris = @("https://localhost") } - Write-ColoredVerbose "WhatIf: Simulating creation of application: $DisplayName" } - - if ($PSCmdlet.ShouldProcess("Service principal '$DisplayName'", "Create")) { - $spParams = @{ - AppId = $newApp.AppId - DisplayName = $DisplayName - } - $newSp = New-EntraServicePrincipal @spParams - Write-ColoredVerbose "Created new service principal for application: $DisplayName" - } - else { - # Handle -WhatIf scenario - $newSp = [PSCustomObject]@{ - Id = "WhatIf-ServicePrincipalId" - DisplayName = $DisplayName - } - Write-ColoredVerbose "WhatIf: Simulating creation of service principal for application: $DisplayName" + + $newApp = New-EntraApplication @appParams + Write-ColoredVerbose "Created new application: $DisplayName" + + # Create service principal for the application + $spParams = @{ + AppId = $newApp.AppId + DisplayName = $DisplayName } - - return [PSCustomObject]@{ + + $newSp = New-EntraServicePrincipal @spParams + Write-ColoredVerbose "Created new service principal for application: $DisplayName" + + [PSCustomObject]@{ ApplicationId = $newApp.Id ApplicationDisplayName = $newApp.DisplayName ServicePrincipalId = $newSp.Id @@ -167,31 +151,25 @@ function Set-EntraAppRoleToApplicationUser { } } else { + # Get existing service principal $existingSp = Get-EntraServicePrincipal -Filter "appId eq '$($existingApp.AppId)'" -ErrorAction SilentlyContinue - + if (-not $existingSp) { - if ($PSCmdlet.ShouldProcess("Service principal '$DisplayName'", "Create")) { - $spParams = @{ - AppId = $existingApp.AppId - DisplayName = $DisplayName - } - $newSp = New-EntraServicePrincipal @spParams - Write-ColoredVerbose "Created new service principal for existing application: $DisplayName" - } - else { - $newSp = [PSCustomObject]@{ - Id = "WhatIf-ServicePrincipalId" - DisplayName = $DisplayName - } - Write-ColoredVerbose "WhatIf: Simulating creation of service principal for existing application: $DisplayName" + # Create service principal if it doesn't exist + $spParams = @{ + AppId = $existingApp.AppId + DisplayName = $DisplayName } + + $newSp = New-EntraServicePrincipal @spParams + Write-ColoredVerbose "Created new service principal for existing application: $DisplayName" } else { $newSp = $existingSp Write-ColoredVerbose "Service principal already exists for application: $DisplayName" } - - return [PSCustomObject]@{ + + [PSCustomObject]@{ ApplicationId = $existingApp.Id ApplicationDisplayName = $existingApp.DisplayName ServicePrincipalId = $newSp.Id @@ -206,7 +184,6 @@ function Set-EntraAppRoleToApplicationUser { return $null } } - function AssignAppServicePrincipalRoleAssignmentIfNotExists { @@ -239,10 +216,7 @@ function Set-EntraAppRoleToApplicationUser { } # Create new assignment - if ($PSCmdlet.ShouldProcess("Service Principal App Role assignment: AppRole - '$appRoleId' | UserId - '$UserId' | Service Principal - '$servicePrincipalObject.Id'", "Create")) { - $newAssignment = New-EntraServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipalObject.Id -ResourceId $servicePrincipalObject.Id -Id $appRoleId -PrincipalId $UserId - } - + $newAssignment = New-EntraServicePrincipalAppRoleAssignment -ServicePrincipalId $servicePrincipalObject.Id -ResourceId $servicePrincipalObject.Id -Id $appRoleId -PrincipalId $UserId Write-ColoredVerbose "Created new role assignment for user '$UserId' - AppName: '$ApplicationName' with role '$RoleDisplayName'" -Color "Green" return [PSCustomObject]@{ @@ -308,7 +282,6 @@ function Set-EntraAppRoleToApplicationUser { # Add to the typed list $appRolesList.Add($appRole) [void]$createdRoles.Add($appRole) - Write-ColoredVerbose "Created new role definition for '$roleName'" -Color "Green" } @@ -320,10 +293,7 @@ function Set-EntraAppRoleToApplicationUser { Tags = @("WindowsAzureActiveDirectoryIntegratedApp") } - - if ($PSCmdlet.ShouldProcess("Update application '$DisplayName' with AppRole list - '$appRolesList'", "Update")) { - Update-MgApplication @params - } + Update-MgApplication @params Write-ColoredVerbose "Updated application with $($createdRoles.Count) new roles" -Color "Green" return $createdRoles | ForEach-Object { From d0949ed04cd8e92de65a8568a1d05edf6ed054da Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Mon, 3 Mar 2025 20:32:12 +0300 Subject: [PATCH 8/9] End of milestone checkin --- .../Governance/Set-EntraAppRoleToApplicationUser.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 index 51449f0e89..2ab676bcb8 100644 --- a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -9,6 +9,7 @@ function Set-EntraAppRoleToApplicationUser { HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic' which determines the column attribute mapping.", ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] + [Parameter(Mandatory = $true, ParameterSetName = 'ValidateAction')] [ValidateSet("DatabaseorDirectory", "SAPCloudIdentity", "Generic")] [string]$DataSource, @@ -16,6 +17,7 @@ function Set-EntraAppRoleToApplicationUser { HelpMessage = "Path to the input file containing users, e.g., C:\temp\users.csv", ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] + [Parameter(Mandatory = $true, ParameterSetName = 'ValidateAction')] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path $_ })] [System.IO.FileInfo]$FileName, @@ -24,6 +26,7 @@ function Set-EntraAppRoleToApplicationUser { HelpMessage = "Name of the application (Service Principal) to assign roles for", ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] + [Parameter(Mandatory = $true, ParameterSetName = 'ValidateAction')] [ValidateNotNullOrEmpty()] [string]$ApplicationName, @@ -34,7 +37,10 @@ function Set-EntraAppRoleToApplicationUser { [Parameter(Mandatory = $false, ParameterSetName = 'ExportResults', HelpMessage = "Path for the export file. Defaults to current directory.")] - [System.IO.FileInfo]$ExportFileName = (Join-Path (Get-Location) "EntraAppRoleAssignments_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv") + [System.IO.FileInfo]$ExportFileName = (Join-Path (Get-Location) "EntraAppRoleAssignments_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"), + + [Parameter(Mandatory = $false, ParameterSetName = 'ValidateAction')] + [switch]$Validate ) process { @@ -130,6 +136,7 @@ function Set-EntraAppRoleToApplicationUser { } $newApp = New-EntraApplication @appParams + $validationStatus += "New application will be created with displayName - '$DisplayName'" Write-ColoredVerbose "Created new application: $DisplayName" # Create service principal for the application @@ -320,6 +327,7 @@ function Set-EntraAppRoleToApplicationUser { function StartOrchestration { try { + $validationStatus = @() # Import users from the CSV file Write-ColoredVerbose "Importing users from file: $FileName" -Color "Cyan" $users = Import-Csv -Path $FileName From ba1d3b73ccf95dace263d805c4774178585ba41c Mon Sep 17 00:00:00 2001 From: stevemutungi Date: Tue, 4 Mar 2025 15:02:26 +0300 Subject: [PATCH 9/9] Reverts --- .../Governance/Set-EntraAppRoleToApplicationUser.ps1 | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 index 2ab676bcb8..51449f0e89 100644 --- a/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -9,7 +9,6 @@ function Set-EntraAppRoleToApplicationUser { HelpMessage = "Specify the data source type: 'DatabaseorDirectory', 'SAPCloudIdentity', or 'Generic' which determines the column attribute mapping.", ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] - [Parameter(Mandatory = $true, ParameterSetName = 'ValidateAction')] [ValidateSet("DatabaseorDirectory", "SAPCloudIdentity", "Generic")] [string]$DataSource, @@ -17,7 +16,6 @@ function Set-EntraAppRoleToApplicationUser { HelpMessage = "Path to the input file containing users, e.g., C:\temp\users.csv", ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] - [Parameter(Mandatory = $true, ParameterSetName = 'ValidateAction')] [ValidateNotNullOrEmpty()] [ValidateScript({ Test-Path $_ })] [System.IO.FileInfo]$FileName, @@ -26,7 +24,6 @@ function Set-EntraAppRoleToApplicationUser { HelpMessage = "Name of the application (Service Principal) to assign roles for", ParameterSetName = 'Default')] [Parameter(Mandatory = $true, ParameterSetName = 'ExportResults')] - [Parameter(Mandatory = $true, ParameterSetName = 'ValidateAction')] [ValidateNotNullOrEmpty()] [string]$ApplicationName, @@ -37,10 +34,7 @@ function Set-EntraAppRoleToApplicationUser { [Parameter(Mandatory = $false, ParameterSetName = 'ExportResults', HelpMessage = "Path for the export file. Defaults to current directory.")] - [System.IO.FileInfo]$ExportFileName = (Join-Path (Get-Location) "EntraAppRoleAssignments_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"), - - [Parameter(Mandatory = $false, ParameterSetName = 'ValidateAction')] - [switch]$Validate + [System.IO.FileInfo]$ExportFileName = (Join-Path (Get-Location) "EntraAppRoleAssignments_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv") ) process { @@ -136,7 +130,6 @@ function Set-EntraAppRoleToApplicationUser { } $newApp = New-EntraApplication @appParams - $validationStatus += "New application will be created with displayName - '$DisplayName'" Write-ColoredVerbose "Created new application: $DisplayName" # Create service principal for the application @@ -327,7 +320,6 @@ function Set-EntraAppRoleToApplicationUser { function StartOrchestration { try { - $validationStatus = @() # Import users from the CSV file Write-ColoredVerbose "Importing users from file: $FileName" -Color "Cyan" $users = Import-Csv -Path $FileName