EntraAuth.Azure.psm1
|
$script:ModuleRoot = $PSScriptRoot class ServiceTransformAttribute : System.Management.Automation.ArgumentTransformationAttribute { [object] Transform([System.Management.Automation.EngineIntrinsics] $Intrinsics, [object] $InputData) { if ($null -eq $InputData) { return @{ Azure = 'Azure' } } if ($InputData -is [hashtable]) { return $InputData } if ($InputData -is [ordered]) { return $InputData } if ($InputData -is [string]) { return @{ Azure = $InputData } } if ($InputData.Azure) { $map = @{ } if ($InputData.Azure) { $map.Azure = $InputData.Azure } return $map } return @{ Graph = $InputData -as [string] } } } function ConvertTo-ResourceGroup { <# .SYNOPSIS Converts Azure API resource group data into module resource group objects. .DESCRIPTION Transforms resource group data returned by the Azure API into EntraAuth.Azure.ResourceGroup objects. The conversion preserves the original object and adds normalized resource group properties and subscription context. .PARAMETER InputObject The Azure API resource group object to convert. Objects are accepted from the pipeline. .PARAMETER SubscriptionID The subscription ID associated with the resource group, added to the converted object's Subscription property. .EXAMPLE PS C:\> $response | ConvertTo-ResourceGroup -SubscriptionID '00000000-0000-0000-0000-000000000001' Converts each resource group in the Azure API response into an EntraAuth.Azure.ResourceGroup object and records the specified subscription ID. #> [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $InputObject, [string] $SubscriptionID ) begin { $converter = { ConvertTo-PSFHashtable }.GetSteppablePipeline() $converter.Begin($true) } process { if (-not $InputObject) { return } [PSCustomObject]@{ PSTypeName = 'EntraAuth.Azure.ResourceGroup' Name = $InputObject.name ID = $InputObject.id Location = $InputObject.location Tags = $($converter.Process($InputObject.tags)) Properties = $InputObject.properties Subscription = $SubscriptionID Object = $InputObject } } end { $converter.End() } } function Resolve-Subscription { <# .SYNOPSIS Resolves a subscription name or ID to a subscription ID. .DESCRIPTION Returns a supplied subscription ID unchanged or resolves an exact subscription display name through Azure. Name resolution succeeds only when exactly one subscription matches and reports an error through the calling cmdlet for missing or ambiguous names. .PARAMETER Name The subscription display name or subscription ID to resolve. .PARAMETER Services A hashtable containing the service mappings used to query subscriptions. .PARAMETER Cmdlet The calling cmdlet context used to report resolution errors. .EXAMPLE PS C:\> Resolve-Subscription -Name 'Production' -Services $services -Cmdlet $PSCmdlet Resolves the subscription whose display name is Production using the supplied services and returns its subscription ID, reporting any resolution error through the calling cmdlet. #> [OutputType([string])] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [hashtable] $Services, [Parameter(Mandatory = $true)] $Cmdlet ) process { # We don't validate GUIDs - we assume the user knew better # Presumably, bad input would still only lead to subsequent request failing if ($Name -as [guid]) { return $Name } $subscriptions = Get-EaaSubscription -ServiceMap $Services | Where-Object DisplayName -EQ $Name if (@($subscriptions).Count -eq 1) { return $subscriptions.SubscriptionID } if (@($subscriptions).Count -gt 1) { Stop-PSFFunction -Message "Ambiguous Subscription! $Name resolved to $(@($subscriptions).Count) subscriptions ($($subscriptions.SubscriptionID -join ', '))" -Cmdlet $Cmdlet -EnableException $true } Stop-PSFFunction -Message "Invalid Subscription! $Name could not be resolved" -Cmdlet $Cmdlet -EnableException $true } } function Get-EaaLocation { <# .SYNOPSIS Lists Azure locations available to a subscription. .DESCRIPTION Retrieves the Azure locations available to the specified subscription and returns location objects with display, regional, category, pairing, availability zone, and source metadata. Results can be filtered by name and can include Azure extended locations. .PARAMETER Subscription The subscription name or ID whose available locations are retrieved. .PARAMETER Name A wildcard pattern used to filter location names. Defaults to: * .PARAMETER IncludeExtended Includes extended locations in the Azure API response when specified. .PARAMETER ServiceMap Optional hashtable to map service names to specific EntraAuth service instances. Used for advanced scenarios where you want to use something other than the default Azure connection. Example: @{ Azure = 'MyAzure' } This will switch all Azure API calls to use the configuration defined in MyAzure. .EXAMPLE PS C:\> Get-EaaLocation -Subscription '00000000-0000-0000-0000-000000000001' -Name 'eastus*' -IncludeExtended Retrieves standard and extended locations whose names begin with eastus for the specified subscription. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PsfArgumentCompleter('EntraAuth.Azure.Subscription')] [string] $Subscription, [string] $Name = '*', [switch] $IncludeExtended, [ServiceTransformAttribute()] [hashtable] $ServiceMap = @{} ) begin { $services = $script:_serviceSelector.GetServiceMap($ServiceMap) Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cmdlet $PSCmdlet } process { $query = @{ 'api-version' = '2022-12-01' } if ($IncludeExtended) { $query.includeExtendedLocations = $true } Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/locations" -Query $query | Where-Object name -Like $Name | ForEach-Object { [PSCustomObject]@{ PSTypeName = 'EntraAuth.Azure.Location' ID = $_.id Name = $_.name DisplayName = $_.displayName RegionalDisplayName = $_.regionalDisplayName Category = $_.metadata.regionCategory PairedRegion = $_.metadata.pairedRegion.name Metadata = $_.metadata AvailabilityZoneMappings = $_.availabilityZoneMappings SubscriptionID = $subscriptionID Object = $_ } } } } function Get-EaaResourceGroup { <# .SYNOPSIS Retrieves Azure resource groups from a subscription. .DESCRIPTION Retrieves a resource group by name or lists resource groups in the specified Azure subscription. List results can be limited by tag conditions, an Azure filter expression, or a maximum result count. .PARAMETER Subscription The subscription name or ID from which resource groups are retrieved. .PARAMETER Name The exact name of the resource group to retrieve. .PARAMETER Tag A hashtable of tag name and value used to filter listed resource groups. Must not contain more than one tag! Example: @{ Environment = 'Prod' } This will find all Resource Groups in the 'Prod' environment. .PARAMETER Filter An Azure filter expression appended to any conditions supplied through Tags. Example: Environment eq 'Prod' or Generation eq '3' .PARAMETER Top The maximum number of resource groups requested from Azure. .PARAMETER ServiceMap Optional hashtable to map service names to specific EntraAuth service instances. Used for advanced scenarios where you want to use something other than the default Azure connection. Example: @{ Azure = 'MyAzure' } This will switch all Azure API calls to use the configuration defined in MyAzure. .EXAMPLE PS C:\> Get-EaaResourceGroup -Subscription 'Production' -Name 'WebApps' Retrieves the resource group named WebApps from the Production subscription. .EXAMPLE PS C:\> Get-EaaResourceGroup -Subscription '00000000-0000-0000-0000-000000000001' -Tags @{ Environment = 'Prod' } -Top 20 Lists up to 20 resource groups in the 00000000-0000-0000-0000-000000000001 subscription whose Environment tag equals Prod. #> [CmdletBinding(DefaultParameterSetName = 'Filter')] param ( [Parameter(Mandatory = $true)] [PsfArgumentCompleter('EntraAuth.Azure.Subscription')] [string] $Subscription, [Parameter(ParameterSetName = 'ByName')] [PsfValidatePattern('^[-\w\._\(\)]{1,90}$', ErrorMessage = 'Resource Groups must not be longer than 90 characters, not contain whitespace or special characters!')] [string] $Name, [Parameter(ParameterSetName = 'Filter')] [PsfValidateScript({ $_.Count -lt 2 }, ErrorMessage = 'Cannot specify more than one tag!')] [hashtable] $Tag, [Parameter(ParameterSetName = 'Filter')] [string] $Filter, [Parameter(ParameterSetName = 'Filter')] [int] $Top, [ServiceTransformAttribute()] [hashtable] $ServiceMap = @{} ) begin { $services = $script:_serviceSelector.GetServiceMap($ServiceMap) Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cmdlet $PSCmdlet } process { if ($Name) { Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/resourcegroups/$Name" -Query @{ 'api-version' = '2021-04-01' } | ConvertTo-ResourceGroup -SubscriptionID $subscriptionID return } $query = @{ 'api-version' = '2021-04-01' } if ($PSBoundParameters.Keys -contains 'Top') { $query.'$top' = $Top } if ($Filter) { $query['$filter'] = $Filter } elseif ($Tag) { $query['$filter'] = "tagName eq '$($Tag.Keys[0])' and tagValue eq '$($Tag.Values[0])'" } Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$subscriptionID/resourcegroups" -Query $query | ConvertTo-ResourceGroup -SubscriptionID $subscriptionID } } function Get-EaaSubscription { <# .SYNOPSIS Lists the available subscriptions in the tenant. .DESCRIPTION Lists the available subscriptions in the tenant. .PARAMETER Name A wildcard pattern used to filter subscription display names. Defaults to: * .PARAMETER ID The subscription ID of the subscription to retrieve. .PARAMETER ServiceMap Optional hashtable to map service names to specific EntraAuth service instances. Used for advanced scenarios where you want to use something other than the default Azure connection. Example: @{ Azure = 'MyAzure' } This will switch all Azure API calls to use the configuration defined in MyAzure. .EXAMPLE PS C:\> Get-EaaSubscription -Name 'Production*' Lists subscriptions in the currently connected tenant whose display names begin with Production. .EXAMPLE PS C:\> Get-EaaSubscription -ID '00000000-0000-0000-0000-000000000001' Retrieves the subscription with the specified subscription ID. #> [CmdletBinding(DefaultParameterSetName = 'ByName')] param ( [Parameter(ParameterSetName = 'ByName')] [PsfArgumentCompleter('EntraAuth.Azure.Subscription')] [string] $Name = '*', [Parameter(Mandatory = $true, ParameterSetName = 'ByID')] [guid] $ID, [ServiceTransformAttribute()] [hashtable] $ServiceMap = @{} ) begin { $services = $script:_serviceSelector.GetServiceMap($ServiceMap) Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure function ConvertTo-Subscription { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $InputObject ) process { if (-not $InputObject) { return } [PSCustomObject]@{ PSTypeName = 'EntraAuth.Azure.Subscription' DisplayName = $InputObject.DisplayName ID = $InputObject.id SubscriptionID = $InputObject.SubscriptionID TenantID = $InputObject.TenantID State = $InputObject.state AuthorizationSource = $InputObject.authorizationSource ManagedByTenants = $InputObject.managedByTenants SubscriptionPolicies = $InputObject.subscriptionPolicies Object = $InputObject } } } } process { if ($ID) { Invoke-EntraRequest -Service $services.Azure -Path "subscriptions/$ID" -Query @{ 'api-version' = '2022-12-01' } | ConvertTo-Subscription return } Invoke-EntraRequest -Service $services.Azure -Path 'subscriptions' -Query @{ 'api-version' = '2022-12-01' } | Where-Object displayName -Like $Name | ConvertTo-Subscription } } function New-EaaResourceGroup { <# .SYNOPSIS Creates an Azure resource group. .DESCRIPTION Creates a resource group with the specified name and location in an Azure subscription. The new group can optionally be associated with a managing resource and initialized with tags. .PARAMETER Subscription The subscription name or ID in which the resource group is created. .PARAMETER Name The name of the resource group to create. .PARAMETER Location The Azure location in which resource group metadata is stored. .PARAMETER ManagedBy The resource ID, represented as a GUID, of the resource that manages this resource group. .PARAMETER Tags A hashtable of tag names and values assigned to the new resource group. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER ServiceMap Optional hashtable to map service names to specific EntraAuth service instances. Used for advanced scenarios where you want to use something other than the default Azure connection. Example: @{ Azure = 'MyAzure' } This will switch all Azure API calls to use the configuration defined in MyAzure. .EXAMPLE PS C:\> New-EaaResourceGroup -Subscription 'Production' -Name 'WebApps' -Location 'eastus' -Tags @{ Environment = 'Prod' } Creates the WebApps resource group in eastus under the Production subscription and assigns it an Environment tag with the value Prod. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true)] [PsfArgumentCompleter('EntraAuth.Azure.Subscription')] [string] $Subscription, [Parameter(Mandatory = $true)] [PsfValidatePattern('^[-\w\._\(\)]{1,90}$', ErrorMessage = 'Resource Groups must not be longer than 90 characters, not contain whitespace or special characters!')] [string] $Name, [Parameter(Mandatory = $true)] [PsfArgumentCompleter('EntraAuth.Azure.Location')] [string] $Location, [string] $ManagedBy, [hashtable] $Tags, [ServiceTransformAttribute()] [hashtable] $ServiceMap = @{} ) begin { $services = $script:_serviceSelector.GetServiceMap($ServiceMap) Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cmdlet $PSCmdlet } process { $body = @{ location = $Location } if ($ManagedBy) { $body.managedBy = $ManagedBy } if ($Tags) { $body.tags = $Tags } Invoke-PSFProtectedCommand -Action "Create Resource Group $Name in $Location under Subscription $SubscriptionID" -Target $Name -ScriptBlock { Invoke-EntraRequest -Service $services.Azure -Method PUT -Path "subscriptions/$subscriptionID/resourcegroups/$Name" -Query @{ 'api-version' = '2021-04-01' } -Body $body -ContentType 'application/json' | ConvertTo-ResourceGroup -SubscriptionID $subscriptionID } -EnableException $true -PSCmdlet $PSCmdlet } } function Remove-EaaResourceGroup { <# .SYNOPSIS Removes an Azure resource group. .DESCRIPTION Deletes the specified resource group from an Azure subscription. .PARAMETER Subscription The subscription name or ID containing the resource group. .PARAMETER Name The name of the resource group to remove. .PARAMETER ForceDeletion One or more Azure resource types for which deletion is forced when removing the resource group. .PARAMETER ServiceMap Optional hashtable to map service names to specific EntraAuth service instances. Used for advanced scenarios where you want to use something other than the default Azure connection. Example: @{ Azure = 'MyAzure' } This will switch all Azure API calls to use the configuration defined in MyAzure. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Remove-EaaResourceGroup -Subscription 'Development' -Name 'Temporary' -Confirm:$false Removes the Temporary resource group from the Development subscription without prompting for confirmation. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [PsfArgumentCompleter('EntraAuth.Azure.Subscription')] [string] $Subscription, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [PsfValidatePattern('^[-\w\._\(\)]{1,90}$', ErrorMessage = 'Resource Groups must not be longer than 90 characters, not contain whitespace or special characters!')] [string] $Name, [PsfArgumentCompleter('EntraAuth.Azure.ResourceGroup.ForceDeletion')] [string[]] $ForceDeletion, [ServiceTransformAttribute()] [hashtable] $ServiceMap = @{} ) begin { $services = $script:_serviceSelector.GetServiceMap($ServiceMap) Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure } process { $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cmdlet $PSCmdlet $query = @{ 'api-version' = '2021-04-01' } if ($ForceDeletion) { $query.forceDeletionTypes = $ForceDeletion -join ',' } Invoke-PSFProtectedCommand -Action "Deleting Resource Group $Name under Subscription $SubscriptionID" -Target $Name -ScriptBlock { $null = Invoke-EntraRequest -Service $services.Azure -Method DELETE -Path "subscriptions/$subscriptionID/resourcegroups/$Name" -Query $query } -EnableException $true -PSCmdlet $PSCmdlet } } function Set-EaaResourceGroup { <# .SYNOPSIS Updates an Azure resource group. .DESCRIPTION Updates the managing resource or tags of an existing Azure resource group in the specified subscription. .PARAMETER Subscription The subscription name or ID containing the resource group. .PARAMETER Name The name of the resource group to update. .PARAMETER ManagedBy The resource ID, represented as a GUID, of the resource that manages this resource group. .PARAMETER Tags A hashtable of tag names and values that replaces the resource group's tags. .PARAMETER ServiceMap Optional hashtable to map service names to specific EntraAuth service instances. Used for advanced scenarios where you want to use something other than the default Azure connection. Example: @{ Azure = 'MyAzure' } This will switch all Azure API calls to use the configuration defined in MyAzure. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Set-EaaResourceGroup -Subscription 'Production' -Name 'WebApps' -Tags @{ Environment = 'Prod'; Owner = 'Platform' } Updates the WebApps resource group in the Production subscription with the specified Environment and Owner tags. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [PsfArgumentCompleter('EntraAuth.Azure.Subscription')] [string] $Subscription, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [PsfValidatePattern('^[-\w\._\(\)]{1,90}$', ErrorMessage = 'Resource Groups must not be longer than 90 characters, not contain whitespace or special characters!')] [string] $Name, [string] $ManagedBy, [hashtable] $Tags, [ServiceTransformAttribute()] [hashtable] $ServiceMap = @{} ) begin { $services = $script:_serviceSelector.GetServiceMap($ServiceMap) Assert-EntraConnection -Cmdlet $PSCmdlet -Service $services.Azure if (-not ($ManagedBy -or $Tags)) { Stop-PSFFunction -Message "Neither 'ManagedBy' nor 'Tags' were specified, no change possible" -EnableException $true -Cmdlet $PSCmdlet -Category InvalidOperation } } process { $subscriptionID = Resolve-Subscription -Name $Subscription -Services $services -Cmdlet $PSCmdlet $body = @{ } if ($ManagedBy) { $body.managedBy = $ManagedBy } if ($Tags) { $body.tags = $Tags } Invoke-PSFProtectedCommand -Action "Updating Resource Group $Name under Subscription $SubscriptionID" -Target $Name -ScriptBlock { Invoke-EntraRequest -Service $services.Azure -Method PATCH -Path "subscriptions/$subscriptionID/resourcegroups/$Name" -Query @{ 'api-version' = '2021-04-01' } -Body $body -ContentType 'application/json' | ConvertTo-ResourceGroup -SubscriptionID $subscriptionID } -EnableException $true -PSCmdlet $PSCmdlet } } # Commands run on module import go here # E.g. Argument Completers could be placed here Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.ResourceGroup.ForceDeletion' -ScriptBlock { 'Microsoft.Compute/virtualMachines' 'Microsoft.Compute/virtualMachineScaleSets' } -Global Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.Subscription' -ScriptBlock { $param = @{} if ($fakeBoundParameter.ServiceMap) { $param.ServiceMap = $fakeBoundParameter.ServiceMap } Get-EaaSubscription @param | ForEach-Object { @{ Text = $_.SubscriptionID ListItemText = $_.DisplayName Tooltip = $_.DisplayName } } } -Global Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.Location' -ScriptBlock { if (-not $fakeBoundParameter.Subscription) { return } $param = @{ Subscription = $fakeBoundParameter.Subscription } if ($fakeBoundParameter.ServiceMap) { $param.ServiceMap = $fakeBoundParameter.ServiceMap } Get-EaaLocation @param | ForEach-Object { @{ Text = $_.name ListItemText = $_.DisplayName Tooltip = '{0} ({1}) | {2}' -f $_.DisplayName, $_.Name, $_.Category } } } Register-PSFTeppScriptblock -Name 'EntraAuth.Azure.ResourceGroup' -ScriptBlock { if (-not $fakeBoundParameter.Subscription) { return } $param = @{ Subscription = $fakeBoundParameter.Subscription } if ($fakeBoundParameter.ServiceMap) { $param.ServiceMap = $fakeBoundParameter.ServiceMap } Get-EaaResourceGroup @param | ForEach-Object { @{ Text = $_.Name ListItemText = $_.Name Tooltip = '{0} ({1})' -f $_.Name, $_.Location } } } # Module-wide variables go here # For example if you want to cache some data, have some module-wide config settings, etc. ... those could go here # Example: # $script:config = @{ } $script:_services = @{ Azure = 'Azure' } $script:_serviceSelector = New-EntraServiceSelector -DefaultServices $script:_services Export-ModuleMember -Function 'Get-EaaLocation','Get-EaaResourceGroup','Get-EaaSubscription','New-EaaResourceGroup','Remove-EaaResourceGroup','Set-EaaResourceGroup' |