Public/New-RBACforAppEntry.ps1
|
<# .SYNOPSIS Creates or updates Exchange Online RBAC scoping for an Entra application service principal. .DESCRIPTION New-RBACforAppEntry resolves an Entra service principal by display name, AppId, or service principal object id, creates the scoped Unified Group when needed, adds requested members, ensures the Exchange Online service principal exists, and creates Exchange Online RBAC role assignments for the application. Unified Group creation/configuration is delegated to New-RBACforAppUnifiedGroup and the Exchange Online service principal step to Register-EXOServicePrincipal. The function supports -WhatIf and -Confirm through SupportsShouldProcess. .PARAMETER RegisteredAppName Display name of the registered application or service principal. This is the default parameter set and must resolve to exactly one service principal. .PARAMETER AppId Application (client) id of the registered application. .PARAMETER SpObjectId Object id of the target service principal. .PARAMETER Members Recipients to add to the Unified Group scope. Values must resolve through Get-Recipient. The default placeholder value is GraphAPI-Dummy. .PARAMETER Role Exchange Online application roles to assign. Short names such as Mail.Send are normalized to Application Mail.Send where supported. .PARAMETER ManagedBy Recipient that will be assigned as the Unified Group owner. .PARAMETER GroupPrefix Prefix used when building the Unified Group name. .PARAMETER BootstrapMember Optional initial member passed during Unified Group creation. .EXAMPLE New-RBACforAppEntry -RegisteredAppName 'Contoso Mail App' -Verbose -WhatIf Shows the planned service principal resolution, Unified Group creation, and RBAC assignment actions without making changes. .EXAMPLE New-RBACforAppEntry -AppId '11111111-2222-3333-4444-555555555555' -Members 'sharedmailbox@contoso.com' -Role 'Mail.Send' -Verbose Resolves the application by AppId, ensures the scoped Unified Group exists, adds the recipient, and creates the Application Mail.Send role assignment. .EXAMPLE New-RBACforAppEntry -SpObjectId '11111111-2222-3333-4444-555555555555' -Role 'Application Calendars.Read','Application Contacts.Read' -GroupPrefix 'Um365Prod' Uses the service principal object id directly and creates multiple application role assignments scoped to the generated Unified Group. .OUTPUTS PSCustomObject Returns a summary object with resolved identity, Unified Group name, normalized roles, assignment names, warnings, and errors. .NOTES Requires Microsoft Graph and Exchange Online cmdlets used by Get-MgServicePrincipal, New-ServicePrincipal, Get-UnifiedGroup, Set-UnifiedGroup, Add-UnifiedGroupLinks, and New-ManagementRoleAssignment. #> function New-RBACforAppEntry { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'ByName')] param( # Default: resolve by displayName (can be non-unique; will error if ambiguous) [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByName')] [Alias('DisplayName','Name')] [ValidateNotNullOrEmpty()] [string] $RegisteredAppName, # Alternative: resolve by AppId (GUID) [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'ByAppId')] [Alias('ClientId','ApplicationId')] [ValidatePattern('^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$')] [string] $AppId, # Alternative: resolve by Service Principal ObjectId (GUID) [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'BySpObjectId')] [Alias('Id','ObjectId','ServicePrincipalId')] [ValidatePattern('^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$')] [string] $SpObjectId, [Parameter(Position = 1)] [string[]] $Members = @("GraphAPI-Dummy"), [Parameter(Position = 2)] [ValidateNotNullOrEmpty()] [string[]] $Role = @("Application Mail.Send"), [Parameter()] [ValidateNotNullOrEmpty()] [string] $ManagedBy = "GraphAPI-Dummy-owner", [Parameter()] [ValidateNotNullOrEmpty()] [string] $GroupPrefix = "Um365RAo1", # Optional placeholder member (dont validate as email) [Parameter()] [string] $BootstrapMember = "GraphAPI-Dummy" ) begin { $shortRoleMap = Get-AppRoleMap $tenantid = Get-MgContext | Select-Object -ExpandProperty TenantId } process { $result = [ordered]@{ ParameterSet = $PSCmdlet.ParameterSetName IdentityInput = $RegisteredAppName ResolvedDisplay = $null AppId = $null SpObjectId = $null TenantId = $tenantid UnifiedGroupName = $null OwnerRequested = $ManagedBy OwnerAdded = $null MembersRequested = @($Members) MembersAdded = @() FilteredMembers = @() RolesNormalized = @() RoleAssignments = @() RoleAssignmentsName = @() Warnings = @() Errors = @() } try { # --- Resolve service principal depending on parameter set $sp = $null switch ($PSCmdlet.ParameterSetName) { 'BySpObjectId' { $sp = Get-MgServicePrincipal -ServicePrincipalId $SpObjectId -ErrorAction Stop } 'ByAppId' { $filter = "appId eq `'$AppId`'" $matchesRes = @(Get-MgServicePrincipal -Filter $filter -ErrorAction Stop) if ($matchesRes.Count -eq 0) { throw "No service principal found for AppId '$AppId'." } if ($matchesRes.Count -gt 1) { throw "Unexpected: multiple service principals for AppId '$AppId'." } $sp = $matchesRes[0] } 'ByName' { $filter = "displayName eq `'$RegisteredAppName`'" $matchesRes = @(Get-MgServicePrincipal -Filter $filter -ErrorAction Stop) if ($matchesRes.Count -eq 0) { throw "No service principal found for displayName '$RegisteredAppName'." } if ($matchesRes.Count -gt 1) { $names = ($matchesRes | Select-Object -First 10 -ExpandProperty Id) -join ', ' throw "Ambiguous displayName '$RegisteredAppName' matched $($matchesRes.Count) service principals. Re-run with -AppId or -SpObjectId. Example SP objectIds: $names" } $sp = $matchesRes[0] } } $result.ResolvedDisplay = $sp.DisplayName $result.AppId = $sp.AppId $result.SpObjectId = $sp.Id # --- Ensure Unified Group for scoping (delegated to New-RBACforAppUnifiedGroup) $umGroupName = "{0}-{1}" -f $GroupPrefix, $sp.DisplayName $umGroupName = Get-SafeName($umGroupName) $result.UnifiedGroupName = $umGroupName Write-Verbose -Message ("Checking Unified Group '{0}' for service principal '{1}' ({2})." -f $umGroupName, $sp.DisplayName, $sp.Id) $ugResult = New-RBACforAppUnifiedGroup -Name $umGroupName -ManagedBy $ManagedBy -BootstrapMember $BootstrapMember -WarningVariable ugWarnings foreach ($w in $ugWarnings) { if ([string]$w.Message -like '*already exists*') { $result.Warnings += [string]$w.Message } } if ($ugResult) { $result.OwnerRequested = $ugResult.OwnerRequested $result.OwnerAdded = $ugResult.OwnerAdded } # --- Add members $currentUserUpn = $null try { $connectionInfo = Get-MgContext -ErrorAction Stop $currentUserUpn = $connectionInfo.Account } catch { $result.Warnings += "Could not retrieve current connection user via get-mgContext; connection user filtering will be skipped. Error: $($_.Exception.Message)" Write-Warning -Message "Could not retrieve current connection user via get-mgContext; connection user filtering will be skipped." } foreach ($member in $Members) { if (-not $member) { continue } $rec = Get-Recipient -Identity $member -ErrorAction SilentlyContinue if (-not $rec) { $result.Warnings += "Recipient not found for '$member' (skipped)." continue } if ($currentUserUpn -and ($member -ieq $currentUserUpn)) { # if ($currentUserUpn -and ($rec.PrimarySmtpAddress -ieq $currentUserUpn -or $member -ieq $currentUserUpn)) { $result.FilteredMembers += [string]$rec.PrimarySmtpAddress $filterWarning = "Current connection user '$currentUserUpn' was found in the members list and has been filtered out." $result.Warnings += $filterWarning Write-Warning -Message $filterWarning continue } if ($PSCmdlet.ShouldProcess("UnifiedGroup $umGroupName", "Add member $($rec.PrimarySmtpAddress)")) { Add-UnifiedGroupLinks -Identity $umGroupName -LinkType Members -Links $rec.PrimarySmtpAddress -ErrorAction Stop } $result.MembersAdded += [string]$rec.PrimarySmtpAddress } # --- Ensure EXO ServicePrincipal extension (delegated to Register-EXOServicePrincipal) $exoSpDisplay = "{0}_SP" -f $sp.DisplayName $null = Register-EXOServicePrincipal -AppId $sp.AppId -ObjectId $sp.Id -DisplayName $exoSpDisplay # --- Role assignments $rolesNormalized = foreach ($r in @($Role)) { Get-NormalizeRole $r } $result.RolesNormalized = @($rolesNormalized) foreach ($roleItem in $rolesNormalized) { $ShortRoleName = $shortRoleMap[$roleItem] $rbacNameBase = Get-SafeName -s ("{0}-{1}" -f $ShortRoleName,$sp.DisplayName) -max 63 $result.RoleAssignmentsName += $rbacNameBase if ($PSCmdlet.ShouldProcess("RBAC role assignment", "Assign '$roleItem' to App '$($sp.DisplayName)' scoped to '$umGroupName'")) { $assignment = New-ManagementRoleAssignment ` -App $sp.Id ` -Role $roleItem ` -RecipientGroupScope $umGroupName ` -Name $rbacNameBase ` -ErrorAction Stop $result.RoleAssignments += $assignment } } [pscustomobject]$result [pscustomobject]$result | Export-Clixml ('{0}/{1}_{2}.clixml' -f $env:TEMP,$rbacNameBase,(get-date -format s).Replace(':','')) -Verbose } catch { $result.Errors += $_.Exception.Message [pscustomobject]$result } } } |