PSServicePrincipal.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PSServicePrincipal.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName PSServicePrincipal.Import.DoDotSource -Fallback $false if ($PSServicePrincipal_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName PSServicePrincipal.Import.IndividualFiles -Fallback $false if ($PSServicePrincipal_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\preimport.ps1" # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions . Import-ModuleFile -Path "$ModuleRoot\internal\scripts\postimport.ps1" # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'PSServicePrincipal' -Language 'en-US' Function Add-ExchangePermsToSPN.ps1 { <# .SYNOPSIS Applies the Manage.Exchange permissions to a registered application. .DESCRIPTION This function will apply the necessary application permissions needed for Exchange V2 CBA. .PARAMETER DisplayName Display name of the objects you are retrieving. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS c:\> Add-ExchangePermsToSPN -DisplayName 'CompanySPN' Stamp the permissions on a registerd application by application id from the Azure active directory. #> [OutputType('System.String')] [CmdletBinding()] Param ( [parameter(Position = 0, HelpMessage = "ApplicationID used to retrieve an application")] [ValidateNotNullOrEmpty()] [string] $DisplayName, [switch] $EnableException ) try { Write-PSFMessage -Level Host -Message "Exchange.ManageAsApp roll applied to application {0}. To complete setup go to your application in the Azure portal and Grant Admin Consent." -StringValues $DisplayName $O365SvcPrincipal = Get-AzureADServicePrincipal -All $true | Where-object {$_.DisplayName -eq "Office 365 Exchange Online"} $reqExoAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" $reqExoAccess.ResourceAppId = $O365SvcPrincipal.AppId $delegatedPermissions = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList "dc50a0fb-09a3-484d-be87-e023b12c6440", "Role" # Manage Exchange As Application $reqExoAccess.ResourceAccess = $delegatedPermissions $ADApplication = get-AzureADApplication -SearchString $DisplayName Set-AzureADApplication -ObjectId $ADApplication.ObjectId -RequiredResourceAccess $reqExoAccess } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException return } } Function Add-RoleToSPN { <# .SYNOPSIS Cmdlet for applying Role Assignments to service principal. .DESCRIPTION Applies the contributor role to a service principal object. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER SpnToProcess Service principal being passed into process. .EXAMPLE PS c:\> Add-RoleToSPN -SpnToProcess $newSPN ArrayList of SPN objects to be processed. #> [OutputType('System.String')] [CmdletBinding()] param( [object] $SpnToProcess, [switch] $EnableException ) foreach ($spn in $SpnToProcess) { Invoke-PSFProtectedCommand -Action "Applying role assignment: Adding Contributor role to SPN" -Target $spn -ScriptBlock { Write-PSFMessage -Level Host -Message "Checking current Role Assignment. Waiting for AD Replication" $checkRole = Get-AzRoleAssignment -ObjectId $spn.id if(-NOT $checkRole) { $newRole = New-AzRoleAssignment -ApplicationId $spn.ApplicationId -RoleDefinitionName "Contributor" -ErrorAction Stop Write-PSFMessage -Level Host -Message "Appling Role Assignment: {0} to {1}" -StringValues $newRole.RoleDefinitionName, $newRole.DisplayName } else { Write-PSFMessage -Level Host -Message "{0} already has this role assignment" -StringValues $spn.DisplayName } } -PSCmdlet $PSCmdlet -Continue -RetryCount 5 -RetryWait 5 -RetryErrorType Microsoft.Rest.Azure.CloudException -EnableException $EnableException } } Function New-ServicePrincipal { <# .SYNOPSIS Cmdlet for creating a single object service principal objects .DESCRIPTION Create a single object Service Principal object based on application id. .PARAMETER ApplicationID Application id of the Azure application you are working on. .PARAMETER CertValue Certificate thumbprint being uploaded to the Azure registerd application. .PARAMETER CreateSPNWithPassword Used when a user supplied password is passed in. .PARAMETER DisplayName Display name of the object we are working on. .PARAMETER StartDate Certificate NotBefore time stamp. .PARAMETER EndDate Certificate NotAfter time stamp. .PARAMETER Cba Switch used to create a registered application, self-signed certificate, upload to the application, applies the correct application roll assignments. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER RegisteredApp Used to create an Azure registered application. .EXAMPLE PS c:\> New-ServicePrincipal -DisplayName 'CompanySPN' Create a new service principal with the display name of 'CompanySPN'. .EXAMPLE PS c:\> New-ServicePrincipal -RegisteredApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Create a new service principal with the application id '34a23ad2-dac4-4a41-bc3b-d12ddf90230e' from the newly created Azure registered application. .EXAMPLE PS c:\> New-ServicePrincipal -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Create a new service principal with the application id '34a23ad2-dac4-4a41-bc3b-d12ddf90230e'. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType('System.String')] [CmdletBinding()] param( [switch] $RegisteredApp, [ValidateNotNullOrEmpty()] [string] $ApplicationID, [ValidateNotNullOrEmpty()] [string] $CertValue, [switch] $CreateSPNWithPassword, [ValidateNotNullOrEmpty()] [string] $DisplayName, [ValidateNotNullOrEmpty()] [DateTime] $StartDate, [ValidateNotNullOrEmpty()] [DateTime] $EndDate, [switch] $Cba, [switch] $EnableException ) try { # We can not create applications or service principals with special characters and spaces via Powershell but can in the azure portal $DisplayName -replace '[\W]', '' if($RegisteredApp -and $ApplicationID) { # Registered Application needs ApplicationID Write-PSFMessage -Level Host -Message "Creating SPN with ApplicationID {0}" -StringValues $newSpn.ApplicationID $password = [guid]::NewGuid() $currentDate = Get-Date # Date here has to be different that cert date passed in $endCurrentDate = ($currentDate.AddYears(1)) $securePassword = New-Object Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential -Property @{ StartDate = $currentDate; EndDate = $endCurrentDate; Password = $password} $newSpn = New-AzADServicePrincipal -ApplicationID $ApplicationID -PasswordCredential $securePassword -ErrorAction Stop $script:roleListToProcess.Add($newSpn) return } } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException return } try { if($DisplayName -and $CreateSPNWithPassword) { $password = Read-Host "Enter Password" -AsSecureString $securePassword = New-Object Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential -Property @{ StartDate = Get-Date; EndDate = Get-Date -Year 2024; Password = $password} $newSPN = New-AzADServicePrincipal -DisplayName $DisplayName -PasswordCredential $securePassword -ErrorAction Stop $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) Write-PSFMessage -Level Host -Message "Creating SPN {0} - Secure Password {1}. WARNING: Backup this key!!! If you lose it you will need to reset the credentials for this SPN!" -StringValues $newSPN.DisplayName, $UnsecureSecret $script:roleListToProcess.Add($newSpn) return } } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException return } try { if(($DisplayName) -and (-NOT $RegisteredApp)) { # Enterprise Application (Service Principal) needs display name because it creates the pair $newSpn = New-AzADServicePrincipal -DisplayName $DisplayName -ErrorAction Stop $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newSpn.Secret) $UnsecureSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) Write-PSFMessage -Level Host -Message "Creating SPN DisplayName {0} and secret {1}. WARNING: Backup this key!!! If you lose it you will need to reset the credentials for this SPN!" -StringValues $DisplayName, $UnsecureSecret $script:roleListToProcess.Add($newSpn) return } } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException return } try { # We enter here from new-selfsigned certificate so we need to accept the data stamps passed in from the certificate if($DisplayName -and $RegisteredApp -and $Cba) { Write-PSFMessage -Level Host -Message "Creating registered application and SPN {0}. Certificate uploaded to Azure application" -StringValues $DisplayName $newSPN = New-AzADServicePrincipal -DisplayName $DisplayName -CertValue $CertValue -StartDate $StartDate -EndDate $EndDate -ErrorAction Stop $script:roleListToProcess.Add($newSpn) return } } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException return } } Function Connect-ToAzureInteractively { <# .SYNOPSIS Cmdlet for making an interactive connections to an Azure tenant and subscription. .DESCRIPTION Make an interactive connections to an Azure tenant and subscription. If interactive connection fails it will default to a manual connection. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS c:\> Connect-ToAzureInteractively Make a connection to an Azure tenant. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType('System.String')] [CmdletBinding()] param( [switch] $EnableException ) # Can be modified by end user for interactive login to AzureAD and AzureAZ #$TenantID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxx-xxxxxx' #$ApplicationID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxx-xxxxxx' $CertThumbprint = (Get-ChildItem cert:\CurrentUser\My\ | Where-Object {$_.Subject -eq "CN=PSServicePrincipal" }).Thumbprint if(-NOT $script:AdSessionFound) { if($PSVersionTable.PSEdition -eq "Core") { Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6." -Once "PS Core Doesn't work with AzureAD" $script:runningOnCore = $true } elseif($PSVersionTable.PSEdition -eq "Desktop") { try { $script:AdSessionInfo = Connect-AzureAD -TenantId $TenantID -ApplicationId $ApplicationID -CertificateThumbprint $CertThumbprint -ErrorAction Stop Write-PSFMessage -Level Host -Message "Connected to AzureAD with automatic logon" -Once "Automatically connected to AzureAD" $script:AdSessionFound = $true } catch { Write-PSFMessage -Level Host -Message "Automatic logon to AzureAD failed. Defaulting to interactive connection" -Once "Interactive Logon Failed" $script:AdSessionFound = $false try { $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzureAD" $script:AdSessionInfo = Connect-AzureAD -Credential $Credentials -ErrorAction Stop $script:AdSessionFound = $true Write-PSFMessage -Level Host -Message "Connected to AzureAD successful" -Once "Interactive Logon Successful" } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException } } } } if(-NOT $script:AzSessionFound) { try { $script:AzSessionInfo = Connect-AzAccount -ServicePrincipal -TenantId $TenantID -ApplicationId $ApplicationID -CertificateThumbprint $CertThumbprint -ErrorAction Stop Write-PSFMessage -Level Host -Message "Connected to AzureAZ with automatic logon" -Once "Automatically connected to AzureAZ" $script:AzSessionFound = $true } catch { Write-PSFMessage -Level Host -Message "Automatic logon to AzureAZ failed. Defaulting to interactive connection" -Once "Interactive Logon Failed" $script:AzSessionFound = $false try { $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzAccount" $script:AzSessionInfo = Connect-AzAccount -Credential $Credentials -ErrorAction Stop $script:AzSessionFound = $true Write-PSFMessage -Level Host -Message "Connected to AzureAZ successful" -Once "Interactive Logon Successful" } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException } } } } Function Connect-ToCloudTenant { <# .SYNOPSIS Makes connections to an Azure tenant and subscription. .DESCRIPTION Connect to an Azure tenant and subscription. .PARAMETER Reconnect Used to force a new connection to an Azure tenant. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS c:\> Connect-ToCloudTenant -Reconnect Makes a connection to an Azure tenant or reconnect to another specified tenant. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType('System.String')] [CmdletBinding()] param( [switch] $Reconnect, [switch] $EnableException ) try { if($Reconnect) { Write-PSFMessage -Level Host -Message "Forcing a reconnection to Azure" -Once "Forcing Connection" $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzureAD" $script:AdSessionInfo = Connect-AzureAD -Credential $Credentials -ErrorAction Stop $script:AdSessionFound = $true Write-PSFMessage -Level Host -Message "Connected to AzureAD successful as {0}" -StringValues $Credentials.UserName -Once "AzureAD Logon Successful" $Credentials = Get-Credential -Message "Please enter your credentials for Connect-AzAccount" $script:AzSessionInfo = Connect-AzAccount -Credential $Credentials -ErrorAction Stop $script:AzSessionFound = $true Write-PSFMessage -Level Host -Message "Connected to AzureAZ successful as {0}" -StringValues $Credentials.UserName -Once "AzureAZ Logon Successful" return } $script:AdSessionInfo = Get-AzureADCurrentSessionInfo -ErrorAction Stop Write-PSFMessage -Level Host -Message "AzureAD session found! Connected as {0} - Tenant {1} with Environment as {2}" -StringValues $script:AdSessionInfo.Account.Id, $script:AdSessionInfo.Tenant.Id, $script:AdSessionInfo.Environment.Name -Once "AD Connection Found" $script:AdSessionFound = $true } catch { Write-PSFMessage -Level Verbose -Message "No existing prior AzureAD connection." -Once "No Prior Connection" $script:AdSessionFound = $false Connect-ToAzureInteractively } try { Write-PSFMessage -Level Host -Message "Checking for an existing AzureAZ connection" -Once "No ADConnection" $script:AzSessionInfo = Get-AzContext if(-NOT $script:AzSessionInfo) { Write-PSFMessage -Level Host -Message "No existing prior AzureAZ connection." -Once "No AZ Connection" $script:AzSessionFound = $false Connect-ToAzureInteractively } else { Write-PSFMessage -Level Host -Message "AzureAZ session found! Connected to {0} as {1} - Tenant {2} - Environment as {3}" -StringValues $script:AzSessionInfo.Name, $script:AzSessionInfo.Account, $script:AzSessionInfo.Tenant, $script:AzSessionInfo.Environment.Name -Once "AZ Connection found" $script:AzSessionFound = $true } } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException } } Function Get-EnterpriseApplication { <# .SYNOPSIS Function for retrieving Azure active directory enterprise application. .DESCRIPTION Retrieve a single or group of enterprise applications from the Azure active directory. .PARAMETER ApplicationID ApplicationId of the object(s) being returned. .PARAMETER DisplayName DisplayName of the object(s) being returned. .PARAMETER ObjectID ObjectId of the object being(s) returned. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS c:\> Get-EnterpriseApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Return an Azure active directory enterprise application by ApplicationID. .EXAMPLE PS c:\> Get-EnterpriseApp -DisplayName CompanySPN Return an Azure active directory enterprise application by DisplayName. .EXAMPLE PS c:\> Get-EnterpriseApp -ObjectID 94b26zd1-fah2-1a25-bsc5-7h3d6j3s5g3h Return an Azure active directory enterprise application by ObjectID. #> [OutputType('System.String')] [CmdletBinding()] Param ( [parameter(HelpMessage = "DisplayName used to return enterprise application objects")] [string] $DisplayName, [parameter(HelpMessage = "ApplicationId used to return enterprise application objects")] [guid] $ApplicationId, [parameter(HelpMessage = "ObjectId used to return enterprise application objects")] [String] $ObjectId, [switch] $EnableException ) $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include DisplayName, ObjectId, ApplicationId if((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)){Connect-ToAzureInteractively} Invoke-PSFProtectedCommand -Action "Enterprise application retrieved!" -Target $parameter.Values -ScriptBlock { Get-AzADApplication @parameter -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet } Function Get-LogFolder { <# .SYNOPSIS Logs module information to output and debug logging folders. .DESCRIPTION Open the logging or debug logging directory. .PARAMETER LogFolder Tab complete allowing you to open the module debug logging folder which contains indepth debug logs for the logging provider. .EXAMPLE PS c:\> Get-LogFolder -LogFolder OutputLoggingFolder Open the output log folder. .EXAMPLE PS c:\> Get-LogFolder -LogFolder DebugLoggingFolder Open the debug output log folder. #> [CmdletBinding()] Param ( [string] $LogFolder ) switch ($LogFolder) { "OutputLoggingFolder" { $loggingFolder = Get-PSFConfigValue -FullName "PSServicePrincipal.Logging.PSServicePrincipal.LoggingFolderPath" Invoke-PSFProtectedCommand -Action "Invoking folder item" -Target $parameter.Values -ScriptBlock { Write-PSFMessage -Level Host -Message "Openning default logging foider {0}" -StringValues $loggingFolder $loggingFolder | Invoke-Item } -EnableException $EnableException -PSCmdlet $PSCmdlet } "DebugLoggingFolder" { $debugFolder = Get-PSFConfigValue -FullName "PSFramework.Logging.FileSystem.LogPath" Invoke-PSFProtectedCommand -Action "Invoking folder item" -Target $parameter.Values -ScriptBlock { Write-PSFMessage -Level Host -Message "Openning default logging foider {0}" -StringValues $debugFolder $debugFolder | Invoke-Item } -EnableException $EnableException -PSCmdlet $PSCmdlet } } } Function Get-RegisteredApplication { <# .SYNOPSIS Function for retrieving Azure active directory registered application. .DESCRIPTION Retrieve an Azure active directory registered application. .PARAMETER DisplayName DisplayName of the object(s) being returned. .PARAMETER ObjectID ObjectId of the object being(s) returned. .PARAMETER SearchString SearchString filter of the object being(s) returned. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS c:\> Get-EnterpriseApp -DisplayName CompanySPN Return an Azure active directory registered application by DisplayName. .EXAMPLE PS c:\> Get-RegisteredApp -SearchString CompanyApp Return an Azure active directory registered application by SearchString. .EXAMPLE PS c:\> Get-EnterpriseApp -ObjectID 94b26zd1-fah2-1a25-bsc5-7h3d6j3s5g3h Return an Azure active directory registered application by ObjectID. #> [OutputType('System.String')] [CmdletBinding(DefaultParameterSetName="DisplayNameSet")] Param ( [parameter(HelpMessage = "Display name used to return registered application objects")] [string] $DisplayName, [parameter(HelpMessage = "ObjectID used to return registered application objects")] [ValidateNotNullOrEmpty()] [string] $ObjectId, [parameter(HelpMessage = "Search filter used to return registered application objects")] [ValidateNotNullOrEmpty()] [string] $SearchString, [switch] $EnableException ) $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include ObjectId, SearchString if((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)){Connect-ToAzureInteractively} Invoke-PSFProtectedCommand -Action "Registered application retrieved!" -Target $parameter.Values -ScriptBlock { Get-AzureADApplication @parameter -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet } Function Get-ServicePrincipalObject { <# .SYNOPSIS Filters active directory service principals by display name. .DESCRIPTION Retrieve a service principal from the Azure Active Directory by display name. .PARAMETER ApplicationID ApplicationId of the object(s) being returned. .PARAMETER DisplayName DisplayName of the object(s) being returned. .PARAMETER ObjectID ObjectId of the object(s) being returned. .PARAMETER SearchString SearchString filter used on object(s) being returned. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS c:\> Get-ServicePrincipalObject -DisplayName CompanySPN Get an Azure active directory enterprise application by DisplayName. .EXAMPLE PS c:\> Get-ServicePrincipalObject -SearchString "Company" Get an Azure active directory enterprise application by using a filter. .EXAMPLE PS c:\> Get-ServicePrincipalObject -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Return an Azure active directory enterprise application by ApplicationID. .EXAMPLE PS c:\> Get-ServicePrincipalObject -ObjectID 94b26zd1-fah2-1a25-bsc5-7h3d6j3s5g3h Get an Azure active directory enterprise application by ObjectID. #> [OutputType('System.String')] [CmdletBinding()] Param ( [parameter(HelpMessage = "DisplayName used to return enterprise application objects")] [string] $DisplayName, [parameter(HelpMessage = "ApplicationId used to return enterprise application objects")] [guid] $ApplicationId, [parameter(HelpMessage = "ObjectId used to return enterprise application objects")] [String] $ObjectId, [parameter(HelpMessage = "SearchString used to return enterprise application objects")] [String] $SearchString, [switch] $EnableException ) $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -Include DisplayName, ObjectId, ApplicationId, SearchString if((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)){Connect-ToAzureInteractively} Write-PSFMessage -Level Host "Retrieving SPN by Object(s)" try { $spnOutput = Get-AzADServicePrincipal @parameter | Select-PSFObject DisplayName, "ServicePrincipalNames as SPN", ApplicationId, "ID as ObjectID", ObjectType, Type } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException } $count = 0 foreach($item in $spnOutput) { $count++ [pscustomobject]@{ PSTypeName = 'PSServicePrincipal.Principal' ItemNumber = $count DisplayName = $item.DisplayName ApplicationID = $item.ApplicationID ObjectID = $item.ObjectID ObjectType = $item.ObjectType Type = $item.Type } } } Function New-SelfSignedCert { <# .SYNOPSIS Cmdlet for creating a self-signed certificate. .DESCRIPTION This function will create a single self-signed certificate and place it in the local user and computer store locations. It will also export the .pfx and .cer files to a location of your choice. .PARAMETER CertificateName Name of the self-signed certificate. .PARAMETER DnsName DNS name on the self-signed certificate. .PARAMETER FilePath File path certificates are exported. .PARAMETER Password Secure password for the self-signed certificate. .PARAMETER RegisteredApp Switch used to create an Azure registered application. .PARAMETER SubjectAlternativeName SubjectAlternativeName on the self-signed certificate. .PARAMETER Cba Switch used to create a registered application, self-signed certificate, upload to the application, applies the correct application roll assignments. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .EXAMPLE PS c:\> New-SelfSignedCert -DnsName yourtenant.onmicrosoft.com -Subject "CN=PSServicePrincipal" -CertificateName MyNewCertificate -FilePath c:\temp\ This will create a new self-signed certificate using a DNS and SubjectAlterntiveName, certificate name and export the certs to the c:\temp location .NOTES You must run PowerShell as an administrator to run this function in order to create the certificate in the LocalMachine certificate store. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [OutputType('System.String')] [CmdletBinding(DefaultParameterSetName="SelfSignedCertSet")] param( [parameter(Position = 0, HelpMessage = "File path used to export the self-signed certificates")] [PSFValidateScript({Resolve-PSFPath $_ -Provider FileSystem -SingleItem}, ErrorMessage = "{0} - is not a legit folder" )] [string] $FilePath = (Get-PSFConfigValue -FullName "PSServicePrincipal.Cert.CertFolder"), [parameter(Mandatory = $True, ParameterSetName ="SelfSignedCertSet", HelpMessage = "Certificate name for self-signed certificate")] [ValidateNotNullOrEmpty()] [string] $CertificateName, [parameter(Mandatory = $True, Position = 1, ParameterSetName ="SelfSignedCertSet", HelpMessage = "DNS name for self-signed certificate")] [ValidateNotNullOrEmpty()] [string] $DnsName, [parameter(Mandatory = $True, Position = 2, ParameterSetName='SelfSignedCertSet', HelpMessage = "SubjectAlternativeName for self-signed certificate")] [ValidateNotNullOrEmpty()] [string] $SubjectAlternativeName, [SecureString] $Password = (Read-Host "Enter your self-signed certificate secure password" -AsSecureString), [parameter(HelpMessage = "Passed in from New-ServicePrincipalObject for registered application and cba cert upload bind")] [switch] $RegisteredApp, [parameter(HelpMessage = "Passed in from New-ServicePrincipalObject for auto cba cert upload to Azure application")] [switch] $Cba, [switch] $EnableException ) try { $CertStore = 'cert:\CurrentUser\my\' $CurrentDate = Get-Date; $EndDate = $currentDate.AddYears(1) Write-PSFMessage -Level Host -Message "Creating new self-signed certficate with DnsName {0} in the following certificate store {1}" -StringValues $DnsName, $certStore $newSelfSignedCert = New-SelfSignedCertificate -certstorelocation $CertStore -Subject $SubjectAlternativeName -Dnsname $DnsName -NotBefore $CurrentDate -NotAfter $EndDate -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -KeySpec KeyExchange -KeyExportPolicy Exportable -KeyUsage KeyEncipherment -KeyProtection None $script:certCounter ++ } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException return } try { if(-NOT (Test-Path -Path $FilePath)) { Write-PSFMessage -Level Host -Message "File path {0} does not exist." -StringValues $FilePath $userChoice = Get-PSFUserChoice -Options "1) Create a new directory", "2) Exit" -Caption "User option menu" -Message "What operation do you want to perform?" switch($UserChoice) { 0 {if(New-Item -Path $FilePath -ItemType Directory){Write-PSFMessage -Level Host -Message "Directory {0} created!" -StringValues $FilePath}} 1 {return} } } } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException return } try { # Export the pfx and cer files $PFXCert = Join-Path $FilePath "$CertificateName.pfx" $CERCert = Join-Path $FilePath "$CertificateName.cer" Write-PSFMessage -Level Host -Message "Exporting self-signed certificates {0} and {1} complete!" -StringValues $PFXCert, $CERCert $path = $certStore + $newSelfSignedCert.thumbprint $null = Export-PfxCertificate -cert $path -FilePath $PFXCert -Password $Password [System.IO.File]::WriteAllBytes((Resolve-PSFPath $CERCert -Provider FileSystem -SingleItem -NewChild ), $newSelfSignedCert.GetRawCertData()) $script:certExportedCounter = 2 if($Cba -and $RegisteredApp) { $keyValue = [System.Convert]::ToBase64String($newSelfSignedCert.GetRawCertData()) New-ServicePrincipal -DisplayName $DisplayName -CertValue $keyValue -StartDate $newSelfSignedCert.NotBefore -EndDate $newSelfSignedCert.NotAfter -Cba -RegisteredApp } } catch { Stop-PSFFunction -Message $_ -Cmdlet $PSCmdlet -ErrorRecord $_ -EnableException $EnableException } } Function New-ServicePrincipalObject { <# .SYNOPSIS PowerShell module for creating, retrieving and removing Azure registered / enterprise applications and service principals. .DESCRIPTION This module will create creating, retrieving and remove Azure registered / enterprise applications and service principals objects that can be used for automation tasks. Enterprise applications created will also have a mirror service principal objects created. Azure registered applications will be linked to a new service principal which can then be use as an authentication mechanism for connecting applications and PowerShell session to an Office 365 and Azure tenant. The PSServicePrincipal logging provider is based on PSFramework: All messages are logged by default 'Documents\PowerShell Script Logs' on Windows and 'Documents/PowerShell Script Logs' on MacOS. There are two logging streams (output and debug). Both streams have respective folders for analysis: You can run Get-LogFolder -LogFolder [OutputLoggingFolder] and [DebugLoggingFolder] to access either logging stream directory. For more information on PSFramework please visit: https://psframework.org/ PSFramework Logging: https://psframework.org/documentation/quickstart/psframework/logging.html PSFramework Configuration: https://psframework.org/documentation/quickstart/psframework/configuration.html PSGallery - PSFramework module - https://www.powershellgallery.com/packages/PSFramework/1.0.19 .PARAMETER CreateSelfSignedCertificate Used when creating a single self-signed certificate to be used with registered and enterprise applications for certificate based connections. .PARAMETER CreateSingleObject Used when creating a single default enterprise application (service principal). .PARAMETER CreateBatchObjects Used when creating a batch of service principals from a text file. .PARAMETER CreateSPNWithAppID Used when creating a service principal and a registered Azure ApplicationID. .PARAMETER CreateSPNWithPassword Used when creating a service principal and a registered Azure application with a user supplied password. .PARAMETER CreateSPNsWithNameAndCert Used when creating a service principal and a registered Azure application using a display name and certificate. .PARAMETER Cba Used to create a registered application, self-signed certificate, upload to the application, applies the correct application roll assignments. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER RegisteredApp Switch used to create an Azure registered application. .PARAMETER Reconnect Used when forcing a new connection to an Azure tenant subscription. .PARAMETER ApplicationID Unique ApplicationId for a service principal in a tenant. Once created this property cannot be changed. .PARAMETER Certificate This parameter is the value of the "asymmetric" credential type. It represents the base 64 encoded certificate. .PARAMETER DisplayName DisplayName of the objects you are retrieving. .PARAMETER NameFile Name of the file that contains the list of service principals being passed in for creation. .PARAMETER ObjectID ObjectId for a service principal in a tenant. Once created this property cannot be changed. .EXAMPLE PS c:\> New-ServicePrincipalObject -Reconnect Force a reconnect to a specific Azure tenant. .EXAMPLE PS c:\> New-ServicePrincipalObject -CreateSelfSignedCertificate Create a basic self-signed certificate to be used with registered and enterprise applications for certificate-based connections. .EXAMPLE PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSingleObject Create a new service principal with a display name of 'CompanySPN' and password (an autogenerated GUID) and creates the service principal based on the application just created. The start date and end date are added to password credential. .EXAMPLE PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSingleObject -CreateSPNWithPassword Create a new Enterprise Application and service principal with a display name of 'CompanySPN' and a (user supplied password) and creates the service principal based on the application just created. The start date and end date are added to password credential. .EXAMPLE PS c:\> New-ServicePrincipalObject -DisplayName CompanyApp -CreateSingleObject -RegisteredApp -Cba Create a registered application with a display name of 'CompanyApp', a self-signed certificate which is uploaded to the application and applies the correct appRoll assignments. .EXAMPLE PS c:\> New-ServicePrincipalObject -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e -CreateSingleObject -CreateSPNWithAppID Create a new Enterprise Application and service principal with the ApplicationID '34a23ad2-dac4-4a41-bc3b-d12ddf90230e'. .EXAMPLE PS c:\> New-ServicePrincipalObject -CreateBatchObjects -NameFile c:\temp\YourDataFile.txt Connect to an Azure tenant and creates a batch of enterprise applications and service principal objects from a file passed in. .EXAMPLE PS c:\> New-ServicePrincipalObject -DisplayName CompanySPN -CreateSPNsWithNameAndCert -CreateSingleObject -Certificate <public certificate as base64-encoded string> Create a new Enterprise Application and service principal with a display name of 'CompanySPN' and certifcate and creates the service principal based on the application just created. The end date is added to key credential. .NOTES When passing in the application ID it is the Azure ApplicationID from your registered application. WARNING: If you do not connect to an Azure tenant when you run Import-Module Az.Resources you will be logged in interactively to your default Azure subscription. After signing in, you will see information indicating which of your Azure subscriptions is active. If you have multiple Azure subscriptions in your account and want to select a different one, get your available subscriptions with Get-AzSubscription and use the Set-AzContext cmdlet with your subscription id. INFORMATION: The default parameter set uses default values for parameters if the user does not provide any. For more information on the default values used, please see the description for the given parameters below. This cmdlet can assign a role to the service principal with the Role 'Contributor'. Roles are applid at the end of the service principal creation. For more information on Azure RBAC roles please see: https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles Microsoft TechNet Documentation: https://docs.microsoft.com/en-us/powershell/module/az.resources/new-azadserviceprincipal?view=azps-3.8.0 #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(DefaultParameterSetName='Default')] [OutputType('System.Boolean')] [OutputType('System.String')] param( [parameter(ParameterSetName = "Reconnect")] [switch] $Reconnect, [parameter(Mandatory = $True, ParameterSetName = "DisplayName", HelpMessage = "Display name used to create or delete an SPN or application")] [ValidateNotNullOrEmpty()] [string] $DisplayName, [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch indicating to create a single service principal or application")] [switch] $CreateSingleObject, [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for indicating to create self-signed certificate and upload it to the Azure application")] [switch] $Cba, [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for indicating we want to create a Registered Azure application")] [switch] $RegisteredApp, [parameter(ParameterSetName = "CreateSelfSignedCertificate")] [switch] $CreateSelfSignedCertificate, [parameter(Mandatory = $True, ParameterSetName='CreateBatchObjects', HelpMessage = "Switch used to create batch objects")] [switch] $CreateBatchObjects, [parameter(Mandatory = $True, ParameterSetName='CreateBatchObjects', HelpMessage = "Name file used to create a batch of spn's")] [ValidateNotNullOrEmpty()] [string] $NameFile, [parameter(ParameterSetName = "ID", HelpMessage = "ApplicationID used to create or delete an SPN or application")] [ValidateNotNullOrEmpty()] [Guid] $ApplicationID, [parameter(ParameterSetName = "ID", HelpMessage = "Switch for creating spn with an ApplicationID")] [switch] $CreateSPNWithAppID, [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for creating spn with a certificate and Base64 string")] [switch] $CreateSPNsWithNameAndCert, [parameter(ParameterSetName = "DisplayName", HelpMessage = "Switch for creating spn with a password")] [switch] $CreateSPNWithPassword, [parameter(HelpMessage = "Certificate parameter for a created spn")] [ValidateNotNullOrEmpty()] [string] $Certificate, [parameter(HelpMessage = "Enable exception logging")] [switch] $EnableException ) Process { $script:certCounter = 0 $script:certExportedCounter = 0 $script:runningOnCore = $false $script:AzSessionFound = $false $script:AdSessionFound = $false $script:AzSessionInfo = $null $script:AdSessionInfo = $null $script:roleListToProcess = New-Object -Type System.Collections.ArrayList $requiredModules = @("AzureAD", "Az.Accounts", "Az.Resources") Foreach($module in $requiredModules) { if(-NOT (Get-Module -Name $module)) { Import-Module $module; Write-PSFMessage -Level Verbose -Message "Importing required modules {0}" -StringValues $module } } $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Include Reconnect Write-PSFMessage -Level Host -Message "Starting script run: {0}" -StringValues (Get-Date) if($DumpCerts) { Write-PSFMessage -Level Host -Message "Opening CurenntUser and LocalMachine Certificate Stores" Get-ChildItem Cert:\CurrentUser\My | Sort-Object Get-ChildItem Cert:\LocalMachine\My | Sort-Object return } if($CreateSelfSignedCertificate) { Invoke-PSFProtectedCommand -Action "Attempting to create self-signed certificate!" -Target $parameter.Values -ScriptBlock { New-SelfSignedCert @parameter -EnableException -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet } Invoke-PSFProtectedCommand -Action "Attempting to create self-signed certificate!" -Target $parameter.Values -ScriptBlock { Connect-ToCloudTenant @parameters -EnableException return } -EnableException $EnableException -PSCmdlet $PSCmdlet if($CreateBatchObjects) { Write-PSFMessage -Level Host -Message "Testing access to {0}" -StringValues $NameFile if(-NOT (Test-Path -Path $NameFile)) { Stop-PSFFunction -Message "ERROR: File problem. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ return } else { Write-PSFMessage -Level Host -Message "{0} accessable. Reading file contents" -StringValues $NameFile $objectsToCreate = Get-Content $NameFile | Where-Object {$_ -ne ""} # Validate that we have data and if we dont we exit out if(0 -eq $objectsToCreate.Length) { Stop-PSFFunction -Message "Error with imported content. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ return } } # Check to make sure we have the list of objects to process if(-NOT $objectsToCreate) { Write-PSFMessage -Level Warning "ERROR: No list of objects found!" return } Write-PSFMessage -Level Host -Message "Object list DETECTED! Staring batch creation of SPN's" if($RegisteredApp) { Write-PSFMessage -Level Host -Message "Creating batch registered Applications" if(-NOT $script:runningOnCore) { foreach($DisplayName in $objectsToCreate) { try { $newApp = New-AzureADApplication -DisplayName $DisplayName -ErrorAction SilentlyContinue -ErrorVariable ProcessError } catch { Stop-PSFFunction -Message "ERROR creating batch spn's" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ return } if($newApp) { Write-PSFMessage -Level Host -Message "Registered Application created: DisplayName: {0} - ApplicationID {1}" -StringValues $newApp.DisplayName, $newApp.AppId $script:roleListToProcess.Add($newApp) # Since we only create an AzureADapplicaiaton we need to create the matching service principal try { New-ServicePrincipal -ApplicationID $newApp.AppID } catch { Stop-PSFFunction -Message "ERROR creating batch spn's" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ return } } elseif($ProcessError){Write-PSFMessage -Level Warning "WARNING: {0}" -StringValues $ProcessError.Exception.Message} } if($script:roleListToProcess.Count -gt 0){Add-RoleToSPN -spnToProcess $script:roleListToProcess} } else { Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6 to create Registered Applications." } } else { Write-PSFMessage -Level Host -Message "Creating batch entperise service principal and applications" foreach($spn in $objectsToCreate){New-ServicePrincipal -DisplayName $spn} if($roleListToProcess.Count -gt 0){Add-RoleToSPN -spnToProcess $script:roleListToProcess} } } if($CreateSingleObject) { try { if($RegisteredApp) { Write-PSFMessage -Level Host -Message "Creating registered applications" if(-NOT $script:runningOnCore) { if($Cba){New-SelfSignedCert -CertificateName $DisplayName -SubjectAlternativeName $DisplayName -Cba -RegisteredApp -EnableException} else{$newApp = New-AzureADApplication -DisplayName $DisplayName -ErrorAction SilentlyContinue -ErrorVariable ProcessError} if($newApp) { Write-PSFMessage -Level Host -Message "Registered Application created: DisplayName: {0} - ApplicationID {1}" -StringValues $newApp.DisplayName, $newApp.AppId New-ServicePrincipal -ApplicationID $newApp.AppId -RegisteredApp # Since we only create an AzureADapplicaiaton we need to create the matching service principal } elseif($ProcessError){Write-PSFMessage -Level Warning "WARNING: $($ProcessError[0].Exception.Message)"} } else { Write-PSFMessage -Level Host -Message "At this time AzureAD PowerShell module does not work on PowerShell Core. Please use PowerShell version 5 or 6 to create Registered Applications." } } elseif($DisplayName -and $CreateSPNWithPassword){New-ServicePrincipal -DisplayName $DisplayName -CreateSPNWithPassword} elseif(($DisplayName) -and (-NOT $RegisteredApp) -and (-NOT $Cba) -and (-NOT $CreateSPNsWithNameAndCert)){New-ServicePrincipal -DisplayName $DisplayName} if($script:roleListToProcess.Count -gt 0) { Add-RoleToSPN -spnToProcess $script:roleListToProcess if($Cba){Add-ExchangePermsToSPN.ps1 -DisplayName $DisplayName} } } catch {Stop-PSFFunction -Message "ERROR: Creating a simple SPN failed" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_} } if($CreateSPNWithAppID) { try { New-ServicePrincipal -ApplicationID $ApplicationID } catch { Stop-PSFFunction -Message "ERROR creating an spn by application id" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ return } } if($CreateSPNsWithNameAndCert) { try { if((-NOT $Certificate)) { Stop-PSFFunction -Message "ERROR: No service principal specified. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet return } else { Write-PSFMessage -Level Host -Message "Creating new SPN {0} and with auto generated certificate key" -StringValues $DisplayName $endDate = Get-Date; $endDate = $currentDate.AddYears(1) $newSPN = New-AzADServicePrincipal -DisplayName $DisplayName -CertValue $Certificate -EndDate $endDate -ErrorAction Stop Add-RoleToSPN -spnToProcess $newSPN } } catch { Stop-PSFFunction -Message "ERROR: No certificate as base64-encoded string specified. Exiting" -EnableException $EnableException -Cmdlet $PSCmdlet -ErrorRecord $_ return } } } end { if($CreateSelfSignedCertificate) { Write-PSFMessage -Level Host -Message "{0} self-signed certificate created sucessfully!" -StringValues $script:certCounter Write-PSFMessage -Level Host -Message "{0} self-signed certificates exported sucessfully!" -StringValues $script:certExportedCounter } Write-PSFMessage -Level Host -Message "End script run: {0}" -StringValues (Get-Date) Write-PSFMessage -Level Host -Message 'Log saved to: "{0}". Run Get-LogFolder to retrieve the output or debug logs.' -StringValues $script:loggingFolder } } Function Remove-ServicePrincipalObject { <# .SYNOPSIS Deletes an single Azure active directory application or service principal. .DESCRIPTION Delete an Application or Service Principal pair from the Azure Active Directory. .PARAMETER ApplicationID ApplicationID of the object you are deleting. .PARAMETER Confirm Stops processing before any changes are made to an object. .PARAMETER DeleteEnterpriseApp Used to delete an Azure enterprise application. .PARAMETER DeleteRegisteredApp Used to delete an Azure registered application. .PARAMETER DeleteSpn Used to delete a Service Principal. .PARAMETER DisplayName DisplayName of the objects you are deleting. .PARAMETER ServicePrincipalName ServicePrincipalName of the objects you are deleting. .PARAMETER EnableException Disables user-friendly warnings and enables the throwing of exceptions. This is less user friendly, but allows catching exceptions in calling scripts. .PARAMETER ObjectID ObjectID of the objects you are deleting. .PARAMETER WhatIf Only displays the objects that would be affected and what changes would be made to those objects (without the worry of modifying those objects) .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteRegisteredApp -DisplayName CompanySPN Delete a registered Azure application using the DisplayName 'CompanySPN'. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteRegisteredApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Delete a registered Azure application using the ApplicationID. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteRegisteredApp -ObjectID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Delete a registered Azure application using the ObjectID. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteEnterpriseApp -DisplayName CompanySPN Delete an enterprise Azure application using the DisplayName 'CompanySPN'. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteEnterpriseApp -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Delete an enterprise Azure application using the ApplicationID. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteEnterpriseApp -ObjectID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Delete an enterprise Azure application using the ObjectID. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteSpn -DisplayName CompanySPN Delete a service principal by the DisplayName. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -ServicePrincipalName http://CompanySPN Delete a service principal by the ServicePrincipalName. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteSpn -ApplicationID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Delete the service principal by the ApplicationID. .EXAMPLE PS c:\> Remove-ServicePrincipalObject -DeleteSpn -ObjectID 34a23ad2-dac4-4a41-bc3b-d12ddf90230e Delete the service principal by the ObjectID. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] [CmdletBinding(SupportsShouldProcess = $True)] Param ( [parameter(HelpMessage = "DisplayName used to delete an application object")] [ValidateNotNullOrEmpty()] [string] $DisplayName, [parameter(HelpMessage = "ApplicationID used to delete an application object")] [ValidateNotNullOrEmpty()] [string] $ApplicationId, [parameter(HelpMessage = "ObjectID used to delete an application object")] [ValidateNotNullOrEmpty()] [string] $ObjectId, [parameter(HelpMessage = "ServicePrincipalName used to delete an application object")] [ValidateNotNullOrEmpty()] [string] $ServicePrincipalName, [parameter(HelpMessage = "Switch used to delete an enterprise object")] [switch] $DeleteEnterpriseApp, [parameter(HelpMessage = "Switch used to delete an registered object")] [switch] $DeleteRegisteredApp, [parameter(HelpMessage = "Switch used to delete a service principal object")] [switch] $DeleteSpn, [switch] $EnableException ) if(-NOT ($DeleteEnterpriseApp -or $DeleteRegisteredApp -or $DeleteSpn)) { Write-PSFMessage -Level Host -Message "You must past in one of the following switches -DeleteEnterpriseApp -DeleteRegisteredApp -or -DeleteSpn" return } $parameter = $PSBoundParameters | ConvertTo-PSFHashtable -include DisplayName, ApplicationId, ObjectId, ServicePrincipalName if((-NOT $script:AzSessionFound) -or (-NOT $script:AdSessionFound)){Connect-ToAzureInteractively} if($DeleteEnterpriseApp) { Invoke-PSFProtectedCommand -Action "Enterprise application delete!" -Target $parameter.Values -ScriptBlock { Remove-AzADApplication @parameter -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet } if($DeleteRegisteredApp) { Invoke-PSFProtectedCommand -Action "Registered application deleted!" -Target $parameter.Values -ScriptBlock { Remove-AzureADApplication @parameter -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet } if($DeleteSpn) { if($parameter.ContainsValue($ServicePrincipalName) -and (-NOT $ServicePrincipalName.Contains('http://'))) { $parameter.ServicePrincipalName = "http://$ServicePrincipalName" } Invoke-PSFProtectedCommand -Action "Service principal deleted!" -Target $parameter.Values -ScriptBlock { Remove-AzADServicePrincipal @parameter -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet $userChoice = Get-PSFUserChoice -Options "1) No", "2) Yes" -Caption "Delete matching Azure enterprise application" -Message "Would you like to delete the matching Azure enterprise application?" switch ($userChoice) { 0 { Write-PSFMessage -Level Host "No application deleted!" return } 1 { # Remove-AzADApplication doesn't accept ServicePrincipal name so convert the parameter binding and set it to DisplayName if($parameter.ContainsValue($ServicePrincipalName) -and ($ServicePrincipalName.Contains('http://'))) { $parameter.DisplayName = $ServicePrincipalName.substring(7) $parameter.Remove('ServicePrincipalName') } elseif($parameter.ContainsValue($ServicePrincipalName) -and (-NOT $ServicePrincipalName.Contains('http://'))) { $parameter.DisplayName = $ServicePrincipalName $parameter.Remove('ServicePrincipalName') } Invoke-PSFProtectedCommand -Action "Removing application!" -Target $parameter.Values -ScriptBlock { Remove-AzADApplication @parameter -ErrorAction Stop } -EnableException $EnableException -PSCmdlet $PSCmdlet } } } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." # Local Certificate Storage $CertFolder = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "\Self-Signed Certificates" if(-NOT (Test-Path -Path $CertFolder)) { New-Item -Path $CertFolder -ItemType Directory} Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Cert.CertFolder' -Value "$($CertFolder)" -Initialize -Validation string -Handler { } -Description "The default path where to save self-signed certificates. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org" Set-PSFConfig -Module 'PSServicePrincipal' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'PSServicePrincipal.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "PSServicePrincipal.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> # Registers the Tab Complete Script Block with options to be passed in when you hit the TAB key Register-PSFTeppScriptblock -Name "PSServicePrincipal.Logs" -ScriptBlock { 'OutputLoggingFolder', 'DebugLoggingFolder' } <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name PSServicePrincipal.alcohol #> Register-PSFTeppArgumentCompleter -Command Get-LogFolder -Parameter LogFolder -Name PSServicePrincipal.Logs New-PSFLicense -Product 'PSServicePrincipal' -Manufacturer 'Dave Goldman' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-04-28") -Text @" Copyright (c) 2020 Dave Goldman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ # Action that is performed on registration of the provider using Register-PSFLoggingProvider $registrationEvent = { } #region Logging Execution # Action that is performed when starting the logging script (or the very first time if enabled after launching the logging script) $begin_event = { function Get-PSServicePrincipalPath { [CmdletBinding()] param ( ) $path = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.FilePath' $logname = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.LogName' $scriptBlock = { param ( [string] $Match ) $hash = @{ '%date%' = (Get-Date -Format 'yyyy-MM-dd') '%dayofweek%' = (Get-Date).DayOfWeek '%day%' = (Get-Date).Day '%hour%' = (Get-Date).Hour '%minute%' = (Get-Date).Minute '%username%' = $env:USERNAME '%userdomain%' = $env:USERDOMAIN '%computername%' = $env:COMPUTERNAME '%processid%' = $PID '%logname%' = $logname } $hash.$Match } [regex]::Replace($path, '%day%|%computername%|%hour%|%processid%|%date%|%username%|%dayofweek%|%minute%|%userdomain%|%logname%', $scriptBlock) } function Write-PSServicePrincipalMessage { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] $Message, [bool] $IncludeHeader, [string] $FileType, [string] $Path, [string] $CsvDelimiter, [string[]] $Headers ) $parent = Split-Path $Path if (-not (Test-Path $parent)) { $null = New-Item $parent -ItemType Directory -Force } $fileExists = Test-Path $Path #region Type-Based Output switch ($FileType) { #region Csv "Csv" { if ((-not $fileExists) -and $IncludeHeader) { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Set-Content -Path $Path -Encoding UTF8 } else { $Message | ConvertTo-Csv -NoTypeInformation -Delimiter $CsvDelimiter | Select-Object -Skip 1 | Add-Content -Path $Path -Encoding UTF8 } } #endregion Csv #region Json "Json" { if ($fileExists) { Add-Content -Path $Path -Value "," -Encoding UTF8 } $Message | ConvertTo-Json | Add-Content -Path $Path -NoNewline -Encoding UTF8 } #endregion Json #region XML "XML" { [xml]$xml = $message | ConvertTo-Xml -NoTypeInformation $xml.Objects.InnerXml | Add-Content -Path $Path -Encoding UTF8 } #endregion XML #region Html "Html" { [xml]$xml = $message | ConvertTo-Html -Fragment if ((-not $fileExists) -and $IncludeHeader) { $xml.table.tr[0].OuterXml | Add-Content -Path $Path -Encoding UTF8 } $xml.table.tr[1].OuterXml | Add-Content -Path $Path -Encoding UTF8 } #endregion Html } #endregion Type-Based Output } $PSServicePrincipal_includeheader = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.IncludeHeader' $PSServicePrincipal_headers = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.Headers' $PSServicePrincipal_filetype = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.FileType' $PSServicePrincipal_CsvDelimiter = Get-PSFConfigValue -FullName 'PSServicePrincipal.Logging.PSServicePrincipal.CsvDelimiter' if ($PSServicePrincipal_headers -contains 'Tags') { $PSServicePrincipal_headers = $PSServicePrincipal_headers | ForEach-Object { switch ($_) { 'Tags' { @{ Name = 'Tags' Expression = { $_.Tags -join "," } } } 'Message' { @{ Name = 'Message' Expression = { $_.LogMessage } } } default { $_ } } } } $PSServicePrincipal_paramWriteLogFileMessage = @{ IncludeHeader = $PSServicePrincipal_includeheader FileType = $PSServicePrincipal_filetype CsvDelimiter = $PSServicePrincipal_CsvDelimiter Headers = $PSServicePrincipal_headers } } # Action that is performed at the beginning of each logging cycle $start_event = { $PSServicePrincipal_paramWriteLogFileMessage["Path"] = Get-PSServicePrincipalPath } # Action that is performed for each message item that is being logged $message_Event = { Param ( $Message ) $Message | Select-Object $PSServicePrincipal_headers | Write-PSServicePrincipalMessage @PSServicePrincipal_paramWriteLogFileMessage } # Action that is performed for each error item that is being logged $error_Event = { Param ( $ErrorItem ) } # Action that is performed at the end of each logging cycle $end_event = { } # Action that is performed when stopping the logging script $final_event = { } #endregion Logging Execution #region Function Extension / Integration # Script that generates the necessary dynamic parameter for Set-PSFLoggingProvider $configurationParameters = { $configroot = "PSServicePrincipal.Logging.PSServicePrincipal" $configurations = Get-PSFConfig -FullName "$configroot.*" $RuntimeParamDic = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary foreach ($config in $configurations) { $ParamAttrib = New-Object System.Management.Automation.ParameterAttribute $ParamAttrib.ParameterSetName = '__AllParameterSets' $AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute] $AttribColl.Add($ParamAttrib) $RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter(($config.FullName.Replace($configroot, "").Trim(".")), $config.Value.GetType(), $AttribColl) $RuntimeParamDic.Add(($config.FullName.Replace($configroot, "").Trim(".")), $RuntimeParam) } return $RuntimeParamDic } # Script that is executes when configuring the provider using Set-PSFLoggingProvider $configurationScript = { $configroot = "PSServicePrincipal.Logging.PSServicePrincipal" $configurations = Get-PSFConfig -FullName "$configroot.*" foreach ($config in $configurations) { if ($PSBoundParameters.ContainsKey(($config.FullName.Replace($configroot, "").Trim(".")))) { Set-PSFConfig -Module $config.Module -Name $config.Name -Value $PSBoundParameters[($config.FullName.Replace($configroot, "").Trim("."))] } } } # Script that returns a boolean value. "True" if all prerequisites are installed, "False" if installation is required $isInstalledScript = { return $true } # Script that provides dynamic parameter for Install-PSFLoggingProvider $installationParameters = { # None needed } # Script that performs the actual installation, based on the parameters (if any) specified in the $installationParameters script $installationScript = { # Nothing to be done - if you need to install your filesystem, you probably have other issues you need to deal with first ;) } #endregion Function Extension / Integration # Configuration settings to initialize if($IsMacOS) { $loggingFolder = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "/Documents/PowerShell Script Logging" Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FilePath' -Value "$($loggingFolder)/%logname%-%date%.csv" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org" Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.LoggingFolderPath' -Value "$($loggingFolder)" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org" } else { $loggingFolder = Join-Path ([Environment]::GetFolderPath("MyDocuments")) "\PowerShell Script Logging" if(-NOT (Test-Path -Path $loggingFolder)) { New-Item -Path $loggingFolder -ItemType Directory} Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.LoggingFolderPath' -Value "$($loggingFolder)" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org" Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FilePath' -Value "$($loggingFolder)\%logname%-%date%.csv" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org" } $configuration_Settings = { Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FilePath' -Value "$($script:loggingFolder)\%logname%-%date%.csv" -Initialize -Validation string -Handler { } -Description "The path to where the logfile is written. Supports some placeholders such as %Date% to allow for timestamp in the name. For full documentation on the supported wildcards, see the documentation on https://psframework.org" Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.Logname' -Value "New-ServicePrincipalObject" -Initialize -Validation string -Handler { } -Description "A special string you can use as a placeholder in the logfile path (by using '%logname%' as placeholder)" Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.IncludeHeader' -Value $true -Initialize -Validation bool -Handler { } -Description "Whether a written csv file will include headers" Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.Headers' -Value @('Timestamp', 'Level', 'FunctionName', 'Message') -Initialize -Validation stringarray -Handler { } -Description "The properties to export, in the order to select them." Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.FileType' -Value "CSV" -Initialize -Validation psframework.logfilefiletype -Handler { } -Description "In what format to write the logfile. Supported styles: CSV, XML, Html or Json. Html, XML and Json will be written as fragments." Set-PSFConfig -Module PSServicePrincipal -Name 'Logging.PSServicePrincipal.CsvDelimiter' -Value "," -Initialize -Validation string -Handler { } -Description "The delimiter to use when writing to csv." Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.Enabled' -Value $true -Initialize -Validation "bool" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].Enabled = $args[0] } } -Description "Whether the logging provider should be enabled on registration" Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.AutoInstall' -Value $false -Initialize -Validation "bool" -Handler { } -Description "Whether the logging provider should be installed on registration" Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.InstallOptional' -Value $true -Initialize -Validation "bool" -Handler { } -Description "Whether installing the logging provider is mandatory, in order for it to be enabled" Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.IncludeModules' -Value @('PSServicePrincipal') -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].IncludeModules = ($args[0] | Write-Output) } } -Description "Module whitelist. Only messages from listed modules will be logged" Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.ExcludeModules' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].ExcludeModules = ($args[0] | Write-Output) } } -Description "Module blacklist. Messages from listed modules will not be logged" Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.IncludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].IncludeTags = ($args[0] | Write-Output) } } -Description "Tag whitelist. Only messages with these tags will be logged" Set-PSFConfig -Module LoggingProvider -Name 'PSServicePrincipal.ExcludeTags' -Value @() -Initialize -Validation "stringarray" -Handler { if ([PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal']) { [PSFramework.Logging.ProviderHost]::Providers['PSServicePrincipal'].ExcludeTags = ($args[0] | Write-Output) } } -Description "Tag blacklist. Messages with these tags will not be logged" } # This is needed to register the PSServicePrincipal logging provider Register-PSFLoggingProvider -Name "PSServicePrincipal" -RegistrationEvent $registrationEvent -BeginEvent $begin_event -StartEvent $start_event -MessageEvent $message_Event -ErrorEvent $error_Event -EndEvent $end_event -FinalEvent $final_event -ConfigurationParameters $configurationParameters -ConfigurationScript $configurationScript -IsInstalledScript $isInstalledScript -InstallationScript $installationScript -InstallationParameters $installationParameters -ConfigurationSettings $configuration_Settings #endregion Load compiled code |