functions/azure/aad/Assert-AzureServicePrincipalForRbac.ps1
# <copyright file="Assert-AzureServicePrincipalForRbac.ps1" company="Endjin Limited"> # Copyright (c) Endjin Limited. All rights reserved. # </copyright> <# .SYNOPSIS Ensures that an Azure AD service principal exists, creating if necessary. Optionally storing the app credential in Azure Key Vault. .DESCRIPTION Ensures that a suitable Azure AD application & service principal exists. Optionally storing the app credential in Azure Key Vault. .PARAMETER Name The display name of the Azure AD service principal. .PARAMETER CredentialDisplayName The label applied to the created/updated credential, which is important for traceability purposes. .PARAMETER KeyVaultName The key vault where that client secret will be stored. .PARAMETER KeyVaultSecretName The key vault secret name that client secret will be stored in. .PARAMETER RotateSecret When specified, the client secret will be regenerated. .PARAMETER UseApplicationCredential When specified, the managed credential will be associated with the App registration, otherwise it will be associated with the Service Principal object. .OUTPUTS Returns a tuple containing a hashtable representing the object describing the Azure AD service principal and it's client secret. Where the client secret is not avilable (e.g. the service principal aleady exists) or the Key Vault functionality is used, '$null' will be returned for this element. e.g. @( @{ <service-principal-definition> }, "<client-secret>" ) #> function Assert-AzureServicePrincipalForRbac { [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'CredentialDisplayName', Justification='Only holds non-sensitive metadata')] param ( [Parameter(ParameterSetName = 'Default', Mandatory = $true)] [Parameter(ParameterSetName = 'KeyVault', Mandatory = $true)] [string] $Name, [int] $PasswordLifetimeDays = 365, [Parameter(Mandatory = $true)] [string] $CredentialDisplayName, [Parameter(ParameterSetName = 'KeyVault', Mandatory = $true)] [string] $KeyVaultName, [Parameter(ParameterSetName = 'KeyVault', Mandatory = $true)] [string] $KeyVaultSecretName, [switch] $RotateSecret, [Parameter()] [switch] $UseApplicationCredential ) $useKeyVault = ($PSCmdlet.ParameterSetName -eq "KeyVault") # Check whether we have a valid AzPowerShell connection if ($useKeyVault) { # Subscription access required for key vault integration _EnsureAzureConnection -AzPowerShell -ErrorAction Stop | Out-Null } else { # No subscription-level access is required _EnsureAzureConnection -AzPowerShell -TenantOnly -ErrorAction Stop | Out-Null } $credentialSecret = $null $existingSp = _getServicePrincipal -DisplayName $Name if (!$existingSp) { if ($PSCmdlet.ShouldProcess($Name, "Create Service Principal")) { # Create a new service principal $createParams = @{ DisplayName = $Name } $newSp = _newServicePrincipal @createParams Write-Host ("Created service principal [ObjectId={0}, ApplicationId={1}]" -f $newSp.Id, $newSp.appId) # Setup the client secret/credential and store it in key vault, if necessary if ($UseApplicationCredential) { $app = _getApplicationForNewAppCredential -DisplayName $Name $handleCredSplat = @{ Application = $app } } else { $handleCredSplat = @{ ServicePrincipal = $newSp } } $credentialSecret = _handleCredential @handleCredSplat -UseKeyVault $useKeyVault } } else { Write-Host ("Service Principal '{0}' already exists [ObjectId={1}, ApplicationId={2}]" -f ` $Name, $existingSp.Id, $existingSp.appId) if ($useKeyVault) { # if using key vault, check whether the specified secret is available $existingSecret = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $KeyVaultSecretName } # rotate the client secret/credential if (($useKeyVault -and !$existingSecret) -or $RotateSecret) { if ($PSCmdlet.ShouldProcess($Name, "Rotate Service Principal Secret")) { Write-Host "Rotating service principal credential [UseKeyVault=$useKeyVault, KeyVaultSecretMissing=$(!$existingSecret), RotateFlag=$RotateSecret]" if ($UseApplicationCredential) { $app = _getApplicationForNewAppCredential -DisplayName $Name $handleCredSplat = @{ Application = $app } } else { $handleCredSplat = @{ ServicePrincipal = $existingSp } } $credentialSecret = _handleCredential @handleCredSplat -UseKeyVault $useKeyVault } } } return ($existingSp ? $existingSp : $newSp),$credentialSecret } #region Helper functions internal to the module function _handleCredential { [CmdletBinding()] param ( [Parameter(Mandatory=$true, ParameterSetName="Application")] [hashtable] $Application, [Parameter(Mandatory=$true, ParameterSetName="ServicePrincipal")] [hashtable] $ServicePrincipal, [Parameter()] [bool] $UseKeyVault ) if ($PSCmdlet.ParameterSetName -eq "Application") { Write-Verbose "Credential will be associated with the App registration" # Generate a new secret attached to the application registration $newCred = $Application | New-AzADAppCredential ` -EndDate ([DateTime]::Now.AddDays($PasswordLifetimeDays)) ` -DisplayName $CredentialDisplayName } else { # Generate a new secret attached to the service principal Write-Verbose "Credential will be associated with the Service Principal" $newCred = $ServicePrincipal | New-AzADServicePrincipalCredential ` -EndDate ([DateTime]::Now.AddDays($PasswordLifetimeDays)) } if ($UseKeyVault) { $appLoginDetails = @{ appId = $Application.appId password = $newCred.SecretText tenant = (Get-AzContext).Tenant.Id } Write-Host "Storing client secret in key vault [VaultName=$KeyVaultName, SecretName=$KeyVaultSecretName]" Set-AzKeyVaultSecret -VaultName $KeyVaultName ` -Name $KeyVaultSecretName ` -SecretValue ($appLoginDetails | ConvertTo-Json | ConvertTo-SecureString -AsPlainText -Force) ` -ContentType "application/json" ` | Out-Null return $null } else { return (ConvertFrom-SecureString $newCred.SecretText -AsPlainText) } } function _getApplication { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] $DisplayName ) $resp = Invoke-AzRestMethod -Uri "https://graph.microsoft.com/v1.0/applications?`$filter=displayName eq '$DisplayName'" $app = $resp.Content | ConvertFrom-Json -AsHashtable -Depth 100 | Select-Object -ExpandProperty value return $app } function _newApplication { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] $DisplayName ) $payload = @{ displayName = $DisplayName} $resp = Invoke-AzRestMethod -Uri "https://graph.microsoft.com/v1.0/applications" ` -Method POST ` -Payload ($payload | ConvertTo-Json) $newAppp = $resp.Content | ConvertFrom-Json -AsHashtable return $newApp } function _getServicePrincipal { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] $DisplayName ) $resp = Invoke-AzRestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$filter=displayName eq '$DisplayName'" $sp = $resp.Content | ConvertFrom-Json -AsHashtable -Depth 100 | Select-Object -ExpandProperty value return $sp } function _newServicePrincipal { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] $DisplayName ) # Check whether an application object already exists with this display name $app = _getApplicationForNewServicePrincipal @PSBoundParameters if (!$app) { # Create a bare application registration, as the appId $app = _newApplication @PSBoundParameters } # Create the service principal $payload = @{ appId = $app.appId} $newSp = Invoke-AzRestMethod -Uri "https://graph.microsoft.com/v1.0/servicePrincipals" ` -Method POST ` -Payload ($payload | ConvertTo-Json) return ($newSp.Content | ConvertFrom-Json -AsHashtable) } # These wrapper functions are required for mocking purposes as '_getApplication' is called in 2 separate scenarios function _getApplicationForNewServicePrincipal { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] $DisplayName ) _getApplication @PSBoundParameters } function _getApplicationForNewAppCredential { [CmdletBinding()] param ( [Parameter(Mandatory=$true)] $DisplayName ) _getApplication @PSBoundParameters } #endregion |