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..0a10df581a --- /dev/null +++ b/module/Entra/Microsoft.Entra/Governance/Set-EntraAppRoleToApplicationUser.ps1 @@ -0,0 +1,551 @@ +# ------------------------------------------------------------------------------ +# 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' which determines the column attribute mapping.")] + [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 $_ })] + [System.IO.FileInfo]$FilePath, + + [Parameter(Mandatory = $true, + HelpMessage = "Name of the application (Service Principal) to assign roles for")] + [ValidateNotNullOrEmpty()] + [string]$ApplicationName, + + [Parameter(Mandatory = $false, + HelpMessage = "Specifies what Microsoft accounts are supported for the application. Options are 'AzureADMyOrg', 'AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount', 'PersonalMicrosoftAccount'")] + [ValidateSet("AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount", "PersonalMicrosoftAccount")] + [string]$SignInAudience = "AzureADMyOrg", + + [Parameter(Mandatory = $false, + 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. Defaults to current directory.")] + [System.IO.FileInfo]$ExportFilePath = (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 SanitizeInput { + param ([string]$Value) + if ([string]::IsNullOrWhiteSpace($Value)) { return $null } + return $Value -replace "'", "''" -replace '\s+', ' ' -replace "`n|`r", "" | ForEach-Object { $_.Trim().ToLower() } + } + + function CreateUserIfNotExists { + param ( + [string]$UserPrincipalName, + [string]$DisplayName, + [string]$MailNickname + ) + + Write-ColoredVerbose -Message "User details: DisplayName $DisplayName | UserPrincipalName: $UserPrincipalName | MailNickname: $MailNickname" -Color "Cyan" + + try { + $params = @{} + $baseUri = "/v1.0/users" + $query = "?`$filter=userPrincipalName eq '$UserPrincipalName'&`$select=Id,CreationType,DisplayName,GivenName,Mail,MailNickName,MobilePhone,OtherMails, UserPrincipalName,EmployeeId,JobTitle" + $params["Method"] = "GET" + $params["Uri"] = "$baseUri{0}" -f $query + $getUserResponse = Invoke-GraphRequest @params + + # User doesn't exist + if($null -eq $getUserResponse.value -or $getUserResponse.value.Count -eq 0){ + $passwordProfile = @{ + ForceChangePasswordNextSignIn = $true + Password = -join (((48..90) + (96..122)) * 16 | Get-Random -Count 16 | % { [char]$_ }) + + } + + $userParams = @{ + displayName = $DisplayName + passwordProfile = $passwordProfile + userPrincipalName = $UserPrincipalName + accountEnabled = $false + mailNickName = $MailNickname + mail = $UserPrincipalName + } + + $userParams = $userParams | ConvertTo-Json + + $newUserResponse = Invoke-GraphRequest -Uri '/v1.0/users?$select=*' -Method POST -Body $userParams + $newUserResponse = $newUserResponse | ConvertTo-Json | ConvertFrom-Json + + Write-ColoredVerbose -Message "Created new user: $UserPrincipalName" -Color "Green" + + return [PSCustomObject]@{ + Id = $newUserResponse.Id + UserPrincipalName = $newUserResponse.UserPrincipalName + DisplayName = $newUserResponse.DisplayName + MailNickname = $newUserResponse.MailNickname + Mail = $newUserResponse.Mail + Status = 'Created' + } + } + else{ + # User exists + Write-ColoredVerbose -Message "User $UserPrincipalName exists." -Color "Green" + return [PSCustomObject]@{ + Id = $getUserResponse.value.id + UserPrincipalName = $getUserResponse.value.userPrincipalName + DisplayName = $getUserResponse.value.displayName + MailNickname = $getUserResponse.value.mailNickname + Mail = $getUserResponse.value.mail + Status = 'Exists' + } + } + } + catch { + Write-Error "Failed to create or verify user $($UserPrincipalName) $_" + return $null + } + } + + function CreateApplicationIfNotExists { + param ( + [string]$DisplayName, + [string]$SignInAudience + ) + + try { + # Check if application exists + + $params = @{} + $baseUri = "/v1.0/applications" + $query = "?`$filter=displayName eq '$DisplayName'" + $params["Method"] = "GET" + $params["Uri"] = "$baseUri{0}" -f $query + $getAppResponse = Invoke-GraphRequest @params + + if ($null -eq $getAppResponse.value -or $getAppResponse.value.Count -eq 0) { + # Create new application + $appParams = @{ + displayName = $DisplayName + signInAudience = $SignInAudience + web = @{ + RedirectUris = @("https://localhost") + } + } + + $appParams = $appParams | ConvertTo-Json + + $newAppResponse = Invoke-GraphRequest -Uri '/v1.0/applications' -Method POST -Body $appParams + $newAppResponse = $newAppResponse | ConvertTo-Json | ConvertFrom-Json + Write-ColoredVerbose "Created new application: $DisplayName" + + # Create service principal for the application + $spParams = @{ + appId = $newAppResponse.AppId + displayName = $DisplayName + } + + $spParams = $spParams | ConvertTo-Json + + $newSPResponse = Invoke-GraphRequest -Uri "/v1.0/servicePrincipals" -Method POST -Body $spParams + $newSPResponse = $newSPResponse | ConvertTo-Json | ConvertFrom-Json + + Write-ColoredVerbose "Created new service principal for application: $DisplayName" + + [PSCustomObject]@{ + ApplicationId = $newAppResponse.Id + ApplicationDisplayName = $newAppResponse.DisplayName + ServicePrincipalId = $newSPResponse.Id + ServicePrincipalDisplayName = $newSPResponse.DisplayName + AppId = $newAppResponse.AppId + Status = 'Created' + } + } + else { + # Get existing service principal + + $params = @{} + $baseUri = "/v1.0/servicePrincipals" + $query = "?`$filter=appId eq '$($getAppResponse.value.AppId)'" + $params["Method"] = "GET" + $params["Uri"] = "$baseUri{0}" -f $query + $getSPResponse = Invoke-GraphRequest @params + $sp = $null + + if ($null -eq $getSPResponse.value -or $getSPResponse.value.Count -eq 0) { + # Create service principal if it doesn't exist + + $spParams = @{ + appId = $getAppResponse.value.AppId + displayName = $DisplayName + } + + $spParams = $spParams | ConvertTo-Json + + $newSPResponse = Invoke-GraphRequest -Uri "/v1.0/servicePrincipals" -Method POST -Body $spParams + $newSPResponse = $newSPResponse | ConvertTo-Json | ConvertFrom-Json + $sp = $newSPResponse + Write-ColoredVerbose "Created new service principal for existing application: $DisplayName" + } + else { + $sp = $getSPResponse.value + Write-ColoredVerbose "Service principal already exists for application: $DisplayName" + } + + [PSCustomObject]@{ + ApplicationId = $getAppResponse.value.Id + ApplicationDisplayName = $getAppResponse.value.DisplayName + ServicePrincipalId = $sp.Id + ServicePrincipalDisplayName = $sp.DisplayName + AppId = $getAppResponse.value.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 + + $params = @{} + $params["Uri"] = "/v1.0/servicePrincipals/$ServicePrincipalId" + $params["Method"] = "GET" + $servicePrincipalObject = Invoke-GraphRequest @params + $appRoleId = ($servicePrincipalObject.AppRoles | Where-Object { $_.displayName -eq $RoleDisplayName }).Id + + $params = @{} + $params["Uri"] = "/v1.0/servicePrincipals/$ServicePrincipalId/appRoleAssignedTo" + $params["Method"] = "GET" + $existingAssignment = (Invoke-GraphRequest @params).value | 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 + + $assignmentParams = @{ + appRoleId = $appRoleId + principalId = $UserId + resourceId = $ServicePrincipalId + } + + $assignmentParams = $assignmentParams | ConvertTo-Json + + $assignmentResponse = Invoke-GraphRequest -Uri "/v1.0/servicePrincipals/$ServicePrincipalId/appRoleAssignments" -Method POST -Body $assignmentParams + $assignmentResponse = $assignmentResponse | ConvertTo-Json | ConvertFrom-Json + + 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 = $assignmentResponse.Id + Status = 'Created' + CreatedDateTime = $assignmentResponse.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 + + $params = @{} + $params["Uri"] = "/v1.0/applications/$ApplicationId" + $params["Method"] = "GET" + $application = Invoke-GraphRequest @params + + if (-not $application) { + Write-Error "Application not found with ID: $ApplicationId" + return $null + } + + # Ensure the existing AppRoles are properly formatted + $existingRoles = @() + if($null -ne $application.AppRoles){ + $existingRoles = $application.AppRoles + } + $appRolesList = New-Object System.Collections.ArrayList + + foreach ($role in $existingRoles) { + $appRolesList.Add($role) + } + + $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 + } + + $memberTypes = $roleToMemberTypeMapping[$roleName] + $allowedMemberTypes = $memberTypes -split "," # Create array from comma separated string + + # Create new AppRole object + $appRole = @{ + 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 = @{ + appRoles = $appRolesList + tags = @("WindowsAzureActiveDirectoryIntegratedApp") + } + + $params = $params | ConvertTo-Json -Depth 10 + + $patchResponse = Invoke-GraphRequest -Uri "/v1.0/applications/$ApplicationId" -Method PATCH -Body $params + $patchResponse = $patchResponse | ConvertTo-Json | ConvertFrom-Json + + 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: $FilePath" -Color "Cyan" + $users = Import-Csv -Path $FilePath + Write-ColoredVerbose "Imported : $($users.Count) users" -Color "Green" + if (-not $users) { + Write-Error "No users found in the provided file: $FilePath" + 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 -SignInAudience $SignInAudience + 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 = @() + $roleToMemberTypeMapping = @{} + + # Extract unique roles + $users | ForEach-Object { + $role = SanitizeInput -Value $_.Role + if ($role -and $role -notin $uniqueRoles) { + $uniqueRoles += $role + $memberType = SanitizeInput -Value $_.memberType + $roleToMemberTypeMapping[$role] = $memberType + } + } + + 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 = SanitizeInput -Value $user.$sourceMatchPropertyName + Write-ColoredVerbose "UPN : $($cleanUserPrincipalName)" -Color "Green" + + if (-not $cleanUserPrincipalName) { + Write-Warning "Skipping user due to invalid userPrincipalName: $($user.$sourceMatchPropertyName)" + continue + } + + $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 = SanitizeInput -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 = SanitizeInput -Value $user.Role + $userRoleType = SanitizeInput -Value $user.memberType + 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 = CreateUserIfNotExists -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 = $userRoleType + 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 ($Export -and $assignmentResults.Count -gt 0) { + try { + Write-ColoredVerbose "Exporting results to: $ExportFilePath" -Color "Cyan" + $assignmentResults | Export-Csv -Path $ExportFilePath -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 | FilePath - $FilePath | DataSource - $DataSource" -Color "Magenta" + # Start orchestration + StartOrchestration + + } +} + 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..6698b35d2f --- /dev/null +++ b/module/docs/entra-powershell-v1.0/Governance/Set-EntraAppRoleToApplicationUser.md @@ -0,0 +1,241 @@ +--- +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 + -FilePath + -ApplicationName + [-SignInAudience ] + [] +``` + +### ExportResults + +```powershell +Set-EntraAppRoleToApplicationUser + -DataSource + -FilePath + -ApplicationName + [-SignInAudience ] + [-Export] + [-ExportFilePath ] + [] +``` + +## 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" -FilePath "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. +- `-FilePath` 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" -FilePath "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. +- `-FilePath` 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" -FilePath "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. +- `-FilePath` 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 `ExportFilePath` 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" -FilePath "C:\temp\users.csv" -ApplicationName "TestApp" -Export -ExportFilePath "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. +- `-FilePath` 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 `ExportFilePath` parameter isn't provided, results are exported in the current location. +- `-ExportFilePath` 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: False +Accept wildcard characters: False +``` + +### -FilePath + +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 +``` + +### -SignInAudience + +Specifies what Microsoft accounts are supported for the application. Options are "AzureADMyOrg", "AzureADMultipleOrgs", "AzureADandPersonalMicrosoftAccount" and "PersonalMicrosoftAccount". + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: AzureADMyOrg +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Export + +Enables export of results into a CSV file. If `ExportFilePath` 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 +``` + +### -ExportFilePath + +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 + +How to [Govern an application's existing users](https://learn.microsoft.com/entra/id-governance/identity-governance-applications-existing-users) + +## Related Links + +[Get-EntraServicePrincipalAppRoleAssignedTo](../Applications/Get-EntraServicePrincipalAppRoleAssignedTo.md) + +[New-EntraServicePrincipalAppRoleAssignment](../Applications/New-EntraServicePrincipalAppRoleAssignment.md) diff --git a/test/Entra/Governance/Set-EntraAppRoleToApplicationUser.Tests.ps1 b/test/Entra/Governance/Set-EntraAppRoleToApplicationUser.Tests.ps1 new file mode 100644 index 0000000000..8913bcc24a --- /dev/null +++ b/test/Entra/Governance/Set-EntraAppRoleToApplicationUser.Tests.ps1 @@ -0,0 +1,216 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ +BeforeAll { + if((Get-Module -Name Microsoft.Entra.Governance) -eq $null){ + Import-Module Microsoft.Entra.Governance + } + Import-Module (Join-Path $PSScriptRoot "..\..\Common-Functions.ps1") -Force + + $newUserScriptblock = { + + #Write-Host "Mocking New-EntraUser with parameters: $($args | ConvertTo-Json -Depth 3)" + return @( + [PSCustomObject]@{ + "DisplayName" = "Mock-User" + "AccountEnabled" = $true + "Mail" = "User@aaabbbcccc.OnMicrosoft.com" + "userPrincipalName" = "User@aaabbbcccc.OnMicrosoft.com" + "DeletedDateTime" = $null + "CreatedDateTime" = $null + "EmployeeId" = $null + "Id" = "bbbbbbbb-1111-2222-3333-cccccccccccc" + "Surname" = $null + "MailNickName" = "User" + "OnPremisesDistinguishedName" = $null + "OnPremisesSecurityIdentifier" = $null + "OnPremisesUserPrincipalName" = $null + "OnPremisesSyncEnabled" = $false + "onPremisesImmutableId" = $null + "OnPremisesLastSyncDateTime" = $null + "JobTitle" = $null + "CompanyName" = $null + "Department" = $null + "Country" = $null + "BusinessPhones" = @{} + "OnPremisesProvisioningErrors" = @{} + "ImAddresses" = @{} + "ExternalUserState" = $null + "ExternalUserStateChangeDateTime" = $null + "MobilePhone" = $null + } + ) + } + + $getApplicationScriptblock = { + return @( + [PSCustomObject]@{ + "AppId" = "aaaaaaaa-1111-2222-3333-cccccccccccc" + "DeletedDateTime" = $null + "Id" = "bbbbbbbb-1111-2222-3333-cccccccccccc" + " DisplayName" = "Mock-App" + "Info" = @{LogoUrl = ""; MarketingUrl = ""; PrivacyStatementUrl = ""; SupportUrl = ""; TermsOfServiceUrl = "" } + "IsDeviceOnlyAuthSupported" = $True + "IsFallbackPublicClient" = $true + "KeyCredentials" = @{CustomKeyIdentifier = @(211, 174, 247); DisplayName = ""; Key = ""; KeyId = "pppppppp-1111-2222-3333-cccccccccccc"; Type = "Symmetric"; Usage = "Sign" } + "OptionalClaims" = @{AccessToken = ""; IdToken = ""; Saml2Token = "" } + "ParentalControlSettings" = @{CountriesBlockedForMinors = $null; LegalAgeGroupRule = "Allow" } + "PasswordCredentials" = @{} + "PublicClient" = @{RedirectUris = $null } + "PublisherDomain" = "aaaabbbbbcccc.onmicrosoft.com" + "SignInAudience" = "AzureADandPersonalMicrosoftAccount" + "Web" = @{HomePageUrl = "https://localhost/demoapp"; ImplicitGrantSettings = ""; LogoutUrl = ""; } + "Parameters" = $args + } + ) + } + + $getServicePrincipalScriptblock = { + return @( + [PSCustomObject]@{ + "Id" = "00aa00aa-bb11-cc22-dd33-44ee44ee44ee" + "DisplayName" = "Windows Update for Business Deployment Service" + "AccountEnabled" = $true + "AddIns" = @{} + "AlternativeNames" = @{} + "AppDescription" = "" + "AppDisplayName" = "Windows Update for Business Deployment Service" + "AppId" = "00001111-aaaa-2222-bbbb-3333cccc4444" + "AppManagementPolicies" = "" + "AppOwnerOrganizationId" = "00aa00aa-bb11-cc22-dd33-44ee44ee44ee" + "AppRoleAssignedTo" = "" + "AppRoleAssignmentRequired" = $false + "AppRoleAssignments" = @() + "AppRoles" = @("22223333-cccc-4444-dddd-5555eeee6666", "33334444-dddd-5555-eeee-6666ffff7777", "44445555-eeee-6666-ffff-7777aaaa8888", "55556666-ffff-7777-aaaa-8888bbbb9999") + "ApplicationTemplateId" = "" + "ClaimsMappingPolicies" = "" + "CreatedObjects" = "" + "CustomSecurityAttributes" = "" + "DelegatedPermissionClassifications"= "" + "Description" = "" + "DisabledByMicrosoftStatus" = "" + "Endpoints" = "" + "FederatedIdentityCredentials" = "" + "HomeRealmDiscoveryPolicies" = "" + "Homepage" = "" + "Info" = "" + "KeyCredentials" = @{} + "LoginUrl" = "" + "LogoutUrl" = "https://deploymentscheduler.microsoft.com" + "MemberOf" = "" + "Notes" = "" + "NotificationEmailAddresses" = @{} + "Oauth2PermissionGrants" = "" + "Oauth2PermissionScopes" = @("22223333-cccc-4444-dddd-5555eeee6666", "33334444-dddd-5555-eeee-6666ffff7777", "44445555-eeee-6666-ffff-7777aaaa8888", "55556666-ffff-7777-aaaa-8888bbbb9999") + "OwnedObjects" = "" + "Owners" = "" + "PasswordCredentials" = @{} + "PreferredSingleSignOnMode" = "" + "PreferredTokenSigningKeyThumbprint"= "" + "RemoteDesktopSecurityConfiguration"= "" + "ReplyUrls" = @{} + "ResourceSpecificApplicationPermissions"= @{} + "SamlSingleSignOnSettings" = "" + "ServicePrincipalNames" = @("61ae9cd9-7bca-458c-affc-861e2f24ba3b") + "ServicePrincipalType" = "Application" + "SignInAudience" = "AzureADMultipleOrgs" + "Synchronization" = "" + "Tags" = @{} + "TokenEncryptionKeyId" = "" + "TokenIssuancePolicies" = "" + "TokenLifetimePolicies" = "" + "TransitiveMemberOf" = "" + "VerifiedPublisher" = "" + "AdditionalProperties" = @{ + "@odata.context" = "https://graph.microsoft.com/v1.0/`$metadata#servicePrincipals/`$entity" + "createdDateTime" = "2023-07-07T14:07:33Z" + } + "Parameters" = $args + } + ) + } + + $newServicePrincipalAppRoleAssignmentScriptblock = { + # Write-Host "Mocking New-MgServicePrincipalAppRoleAssignment with parameters: $($args | ConvertTo-Json -Depth 3)" + return @( + [PSCustomObject]@{ + "DeletedDateTime" = $null + "Id" = "00aa00aa-bb11-cc22-dd33-44ee44ee44ee" + "PrincipalDisplayName" = "Mock-App" + "AppRoleId" = "bbbb1b1b-cc2c-dd3d-ee4e-ffffff5f5f5f" + "CreatedDateTime" = "3/12/2024 11:05:29 AM" + "PrincipalId" = "aaaaaaaa-bbbb-cccc-1111-222222222222" + "Parameters" = $args + } + ) + } + $csv = { + $object1 = [PSCustomObject]@{ + userPrincipalName = 'user1@contoso.com' + displayName = 'User 1' + mailNickname = 'user1' + Role = 'Admin' + memberType = 'users+groups' + } + + $arr += $object1 + return $arr + } + + Mock -CommandName Get-EntraUser -MockWith {} -ModuleName Microsoft.Entra.Governance + Mock -CommandName New-EntraUser -MockWith { $newUserScriptblock } -ModuleName Microsoft.Entra.Governance + Mock -CommandName Get-EntraApplication -MockWith { $getApplicationScriptblock } -ModuleName Microsoft.Entra.Governance + Mock -CommandName Get-MgApplication -MockWith { $getApplicationScriptblock } -ModuleName Microsoft.Entra.Governance + Mock -CommandName New-EntraApplication -MockWith {} -ModuleName Microsoft.Entra.Governance + Mock -CommandName New-EntraServicePrincipal -MockWith {} -ModuleName Microsoft.Entra.Governance + Mock -CommandName Get-EntraServicePrincipal -MockWith { $getServicePrincipalScriptblock } -ModuleName Microsoft.Entra.Governance + Mock -CommandName Get-EntraServicePrincipalAppRoleAssignedTo -MockWith {} -ModuleName Microsoft.Entra.Governance + Mock -CommandName New-EntraServicePrincipalAppRoleAssignment -MockWith { $newServicePrincipalAppRoleAssignmentScriptblock } -ModuleName Microsoft.Entra.Governance + Mock Test-Path { return $true } -ModuleName Microsoft.Entra.Governance + Mock Import-Csv { $csv } -ModuleName Microsoft.Entra.Governance +} + +Describe "Set-EntraAppRoleToApplicationUser" { + Context "Test for Set-EntraAppRoleToApplicationUser" { + It "Should return empty object" { + $path = "C:\Path\To\users.csv" + $result = Set-EntraAppRoleToApplicationUser -DataSource "Generic" -FileName $path -ApplicationName "Mock-App" + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Get-EntraApplication -ModuleName Microsoft.Entra.Governance -Times 1 + Should -Invoke -CommandName New-EntraUser -ModuleName Microsoft.Entra.Governance -Times 1 + Should -Invoke -CommandName Get-EntraServicePrincipal -ModuleName Microsoft.Entra.Governance -Times 1 + Should -Invoke -CommandName New-EntraServicePrincipalAppRoleAssignment -ModuleName Microsoft.Entra.Governance -Times 1 + } + # It "Should contain 'User-Agent' header" { + # $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Set-EntraDirectoryRoleDefinition" + + # $RolePermissions = New-object Microsoft.Open.MSGraph.Model.RolePermission + # $RolePermissions.AllowedResourceActions = @("microsoft.directory/applications/basic/read") + # Set-EntraDirectoryRoleDefinition -UnifiedRoleDefinitionId "00aa00aa-bb11-cc22-dd33-44ee44ee44ee" -RolePermissions $RolePermissions -IsEnabled $false -DisplayName 'Mock-App' -ResourceScopes "/" -Description "Mock-App" -TemplateId "11bb11bb-cc22-dd33-ee44-55ff55ff55ff" -Version 2 + + # $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Set-EntraDirectoryRoleDefinition" + + # Should -Invoke -CommandName Update-MgRoleManagementDirectoryRoleDefinition -ModuleName Microsoft.Entra.Governance -Times 1 -ParameterFilter { + # $Headers.'User-Agent' | Should -Be $userAgentHeaderValue + # $true + # } + # } + # It "Should execute successfully without throwing an error" { + # # Disable confirmation prompts + # $originalDebugPreference = $DebugPreference + # $DebugPreference = 'Continue' + # $RolePermissions = New-object Microsoft.Open.MSGraph.Model.RolePermission + # $RolePermissions.AllowedResourceActions = @("microsoft.directory/applications/basic/read") + + # try { + # # Act & Assert: Ensure the function doesn't throw an exception + # { Set-EntraDirectoryRoleDefinition -UnifiedRoleDefinitionId "00aa00aa-bb11-cc22-dd33-44ee44ee44ee" -RolePermissions $RolePermissions -IsEnabled $false -DisplayName 'Mock-App' -ResourceScopes "/" -Description "Mock-App" -TemplateId "11bb11bb-cc22-dd33-ee44-55ff55ff55ff" -Version 2 -Debug } | Should -Not -Throw + # } finally { + # # Restore original confirmation preference + # $DebugPreference = $originalDebugPreference + # } + # } + + } +}