Last active
April 28, 2025 09:23
-
-
Save jpawlowski/f6ec653d54ea88e144a80c4f9ff8b64f to your computer and use it in GitHub Desktop.
An Azure Automation Runbook that will help to generate new Temporary Access Pass codes for new employees in Microsoft Entra.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
Create a Temporary Access Pass code for new hires that have not set up any Authentication Methods so far | |
.DESCRIPTION | |
This script is intended to be run as an Azure Automation Runbook or as a standalone script. | |
Before generating a Temporary Access Pass, the script checks if the user has set up any Authentication Methods and has reached its hire date. | |
If the user has not set up any Authentication Methods, a Temporary Access Pass is created. | |
Depending on the configuration, the pass is output or sent to the user's manager via email. | |
The email notification can be customized using a template with variables. | |
Automatic email branding is supported by retrieving tenant branding images and information. | |
Based on the manager's mailbox language, the email notification is sent in the appropriate language. | |
As a fallback, the manager's preferredLanguage attribute is used. | |
The script can be triggered by a webhook request, which is validated using an HMAC signature. | |
The webhook request must include a JSON object with the UserId and may optionally include StartDateTime, LifetimeInMinutes, IsUsableOnce, and EmailLanguage. | |
Other parameters are intentionally ignored to prevent sending unexpected emails. | |
The webhook request must be signed using the shared secret key set as Automation Variable 'AuthConfig_WebhookSignatureKey'. | |
An example function for the client request called 'Get-HmacSignedHeaders' can be found in the FUNCTIONS region down below. | |
**Azure Automation & Managed Identity Setup Requirements:** | |
- Run this script in an Azure Automation account using a system-assigned managed identity. | |
- The managed identity must have the following Microsoft Graph API permissions: | |
• User.Read.All | |
• UserAuthenticationMethod.ReadWrite.All | |
• Policy.Read.All | |
• Directory.Read.All | |
• MailboxSettings.Read | |
• Mail.Send | |
- Ensure admin consent is granted for all required permissions. | |
- Configure the 'AuthConfig_WebhookSignatureKey' Automation Variable with the shared secret used for HMAC validation. | |
- Configure the 'TAPConfig_SenderEmailAddress' Automation Variable with the sender email address. | |
.PARAMETER UserId | |
User account identifier. May be an Entra Identity Object ID or User Principal Name (UPN). | |
.PARAMETER StartDateTime | |
The date and time when the Temporary Access Pass becomes available to use. Needs to be in Universal Time (UTC). | |
.PARAMETER LifetimeInMinutes | |
The lifetime of the Temporary Access Pass in minutes starting at StartDateTime. Must be between 10 and 43200 inclusive (equivalent to 30 days). | |
.PARAMETER IsUsableOnce | |
Determines whether the pass is limited to a one-time use. If true, the pass can be used once; if false, the pass can be used multiple times within the Temporary Access Pass lifetime. | |
.PARAMETER SendEmailToManager | |
Send the Temporary Access Pass to the user's manager via email. | |
.PARAMETER SenderEmailAddress | |
The email address of the sender. Can be a user or shared mailbox address. | |
When run interactively, the sender address is automatically set to the signed-in user's email address. | |
When running in Azure Automation, the sender address must either be provided directly, in the webhook configuration, | |
or as Automation Variable TAPConfig_SenderEmailAddress. | |
.PARAMETER EmailLanguage | |
The language to use for the email notification. If not specified, the user's preferred language is used. | |
The built-in template supports English (en), German (de), and French (fr). | |
The parameter may also be set as an Automation Variable TAPConfig_EmailLanguage. | |
.PARAMETER EmailSubject | |
Custom email subject to use when sending notification to manager. | |
If not specified, the default subject is used. | |
Variables may be used, see EmailTemplate parameter for available variables. | |
Multiple languages can be defined using the JSON format: {"en": "English subject", "de": "German subject", "fr": "French subject"} | |
The parameter may also be set as an Automation Variable TAPConfig_EmailSubject. | |
.PARAMETER EmailTitle | |
Custom email title to use when sending notification to manager. | |
If not specified, the default title is used. | |
Variables may be used, see EmailTemplate parameter for available variables. | |
Multiple languages can be defined using the JSON format: {"en": "English title", "de": "German title", "fr": "French title"} | |
The parameter may also be set as an Automation Variable TAPConfig_EmailTitle. | |
.PARAMETER EmailSalutation | |
Custom email salutation to use when sending notification to manager. | |
If not specified, the default salutation is used. | |
Variables may be used, see EmailTemplate parameter for available variables. | |
Multiple languages can be defined using the JSON format: {"en": "English salutation", "de": "German salutation", "fr": "French salutation"} | |
The parameter may also be set as an Automation Variable TAPConfig_EmailSalutation. | |
.PARAMETER EmailClosing | |
Custom email closing phrase to use when sending notification to manager. | |
If not specified, the default closing phrase is used. | |
Variables may be used, see EmailTemplate parameter for available variables. | |
Multiple languages can be defined using the JSON format: {"en": "English closing", "de": "German closing", "fr": "French closing"} | |
The parameter may also be set as an Automation Variable TAPConfig_EmailClosing. | |
.PARAMETER EmailBodyText | |
Custom email body text to use when sending notification to manager. | |
If not specified, the default body text is used. | |
Variables may be used, see EmailTemplate parameter for available variables. | |
Possible text variants are: "TAPCreated", "UserBlocked", "TAPNotEnabled", "TAPExpiredWithOtherMethodsConfigured", "TAPActiveWithOtherMethodsConfigured", "OtherMethodsConfigured". | |
The value must be a JSON string with the format {"en": {"TAPCreated": "English text", "UserBlocked": "English text", ...}, "de": {"TAPCreated": "German text", "UserBlocked": "German text", ...}, ...} | |
The parameter may also be set as an Automation Variable TAPConfig_EmailBodyText. | |
.PARAMETER EmailTemplate | |
Custom email template to use when sending notification to manager. | |
Uses {{ variable }} syntax for variable substitution. | |
Available variables: userGivenName, userName, userPrincipalName, managerName, | |
temporaryAccessPass, lifetimeInMinutes, expirationTime, startDateTime, emailTitle, emailSalutation, emailClosing, emailBodyText, emailBodyAltertBanner, emailBodyFooterHint. | |
Images can be embedded using {{ image:imageName }} placeholders. | |
Multiple languages can be defined using the JSON format: {"en": "English template", "de": "German template", "fr": "French template"} | |
The parameter may also be set as an Automation Variable TAPConfig_EmailTemplate. | |
.PARAMETER EmailImagesJson | |
A JSON string representing image references to embed in the email template. | |
Format: {"imageName1": "base64string1", "imageName2": "base64string2"} | |
Each image can be referenced in the template as {{ image:imageName1 }} | |
Example: '{"logo": "data:image/png;base64,iVBORw0KG..."}' | |
The parameter may also be set as an Automation Variable TAPConfig_EmailImagesJson. | |
.PARAMETER UseHtmlEmail | |
When specified, sends the email as HTML format. Default is true. | |
Set to false to send plain text email. | |
The parameter may also be set as an Automation Variable TAPConfig_UseHtmlEmail. | |
.PARAMETER OutputJson | |
Output the result in JSON format | |
.PARAMETER OutputText | |
Output the Temporary Access Pass only. | |
.PARAMETER Simulate | |
Same as -WhatIf parameter but makes it available for Azure Automation. | |
.PARAMETER WebhookSignatureKey | |
Shared secret used to validate incoming webhook requests using HMAC HTTP request header authorization. | |
When running in Azure Automation, this parameter is automatically populated from the 'AuthConfig_WebhookSignatureKey' automation variable. | |
.PARAMETER WebhookData | |
Webhook data object for Azure Automation. | |
The following parameters may be overridden by the webhook request: | |
UserId, StartDateTime, LifetimeInMinutes, IsUsableOnce, EmailLanguage | |
.NOTES | |
Filename: New-Temporary-Access-Pass-for-Initial-MFA-Setup-V2.ps1 | |
Author: Julian Pawlowski, Workoho GmbH <[email protected]> | |
Version: 2.3.1 | |
#> | |
#Requires -Version 7.4 | |
#Requires -Modules @{ ModuleName='Microsoft.Graph.Authentication'; ModuleVersion='2.0' } | |
using namespace System.Collections.Generic | |
using namespace System.Text | |
[CmdletBinding( | |
SupportsShouldProcess, | |
ConfirmImpact = 'Medium' | |
)] | |
param ( | |
[string]$UserId, | |
[datetime]$StartDateTime, | |
[int64]$LifetimeInMinutes, | |
[bool]$IsUsableOnce = $false, | |
[bool]$SendEmailToManager = $false, | |
[string]$SenderEmailAddress, | |
[string]$EmailLanguage, | |
[string]$EmailSubject, | |
[string]$EmailTitle, | |
[string]$EmailSalutation, | |
[string]$EmailClosing, | |
[string]$EmailBodyText, | |
[string]$EmailTemplate, | |
[string]$EmailImagesJson, | |
[bool]$UseHtmlEmail = $true, | |
[bool]$OutJson = $false, | |
[bool]$OutText = $false, | |
[bool]$Simulate = $false, | |
[securestring]$WebhookSignatureKey, | |
[object]$WebhookData | |
) | |
#region FUNCTIONS | |
function Get-HmacSignedHeaders { | |
<# | |
.SYNOPSIS | |
Generates signed headers for HMAC authentication for Azure Automation webhook requests. | |
.DESCRIPTION | |
Generates signed headers for HMAC authentication based on a shared secret (provided as SecureString), webhook URL, and request body content. | |
Includes timestamp, nonce, and content hash, all covered in the HMAC signature. | |
SecureString is converted as late as possible and securely cleared after use. | |
This function is intended to be used when calling Azure Automation webhooks or other APIs requiring signed requests for enhanced security. | |
------------------------------------------ | |
🔐 Secure Shared Secret Retrieval Options: | |
------------------------------------------ | |
Always retrieve secrets securely from encrypted sources, ensuring they are stored and used as SecureStrings: | |
1️⃣ Azure Key Vault (Recommended for cloud/hybrid environments) | |
Connect-AzAccount -Identity | |
$sharedSecret = (Get-AzKeyVaultSecret -VaultName "<YourVaultName>" -Name "HmacSharedSecret").SecretValue | |
2️⃣ Azure Automation Encrypted Variable (inside Automation Account only) | |
$sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force | |
3️⃣ Windows Credential Manager (if running on Windows, via SecretManagement module) | |
Import-Module Microsoft.PowerShell.SecretManagement | |
$sharedSecret = (Get-StoredCredential -Target "HmacSharedSecret").Password | |
4️⃣ Encrypted local file (protected by ACLs; not recommended for cloud, acceptable in controlled environments) | |
$sharedSecret = (Get-Content -Path "C:\Secrets\HmacSecret.txt" -Raw) | ConvertTo-SecureString -AsPlainText -Force | |
❌ For demonstration purposes only (NEVER hardcode secrets in production): | |
$sharedSecret = ConvertTo-SecureString -String "MySuperSecretKey" -AsPlainText -Force | |
.EXAMPLE | |
# Example usage: | |
$sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force | |
$webhookUrl = "https://<your-webhook-url>/webhooks" | |
$body = '{"param1":"value1"}' | |
$headers = Get-HmacSignedHeaders -SharedSecret $sharedSecret ` | |
-WebhookUrl $webhookUrl ` | |
-Body $body | |
[void]$sharedSecret.Dispose() | |
Invoke-RestMethod -Method POST ` | |
-Uri $webhookUrl ` | |
-Headers $headers ` | |
-Body $body ` | |
-ContentType 'application/json; charset=utf-8' | |
.FUNCTIONALITY | |
Security, HMAC Authentication | |
.NOTES | |
Author: Julian Pawlowski | |
Company Name: Workoho GmbH | |
Created: 2025-03-17 | |
Updated: 2025-03-18 | |
#> | |
param( | |
# Shared secret used for HMAC signature (SecureString) | |
[Parameter(Mandatory)][securestring]$SharedSecret, | |
# Full webhook URL to which the request will be sent | |
[Parameter(Mandatory)][string]$WebhookUrl, | |
# Request body content as a string (usually JSON or similar) | |
[Parameter(Mandatory)][string]$Body, | |
# Algorithm to use for HMAC signature (default: HMACSHA256) | |
[string]$Algorithm = "HMACSHA256", | |
# Optional: Provide custom nonce (for testing/debugging); defaults to a new random GUID if omitted | |
[string]$Nonce | |
) | |
if ($Algorithm -notin @('HMACSHA256', 'HMACSHA512')) { | |
throw "Unsupported algorithm: $Algorithm" | |
} | |
# Parse URL components | |
$uri = [System.Uri]$WebhookUrl | |
$method = "POST" | |
$path = $uri.AbsolutePath | |
$webHost = $uri.Host | |
# Timestamp header (RFC1123 format) | |
$date = (Get-Date).ToUniversalTime().ToString("R") | |
# Compute body hash (SHA256) | |
$bodyBytes = [Text.Encoding]::UTF8.GetBytes($Body) | |
$contentHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes)) | |
# Generate nonce if not provided | |
if (-not $Nonce) { | |
$Nonce = [Guid]::NewGuid().ToString() | |
} | |
# Define signed headers and order | |
$signedHeadersList = @('x-ms-date', 'Host', 'x-ms-content-sha256', 'x-ms-nonce') | |
$signedHeaders = ($signedHeadersList -join ';') | |
# Build canonical message | |
$canonicalMessage = "$method`n$path`n$date;$webHost;$contentHash;$Nonce" | |
$unsecureSecret = $null | |
try { | |
$hmac = New-Object ("System.Security.Cryptography.$Algorithm") | |
# SecureString → plaintext | |
$unsecureSecret = [System.Net.NetworkCredential]::New("", $SharedSecret).Password | |
# Set key and compute signature | |
$hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret) | |
$signature = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage))) | |
# Cleanup HMAC key | |
[Array]::Clear($hmac.Key, 0, $hmac.Key.Length) | |
$hmac = $null | |
} | |
finally { | |
if ($unsecureSecret) { | |
[Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length) | |
$unsecureSecret = $null | |
} | |
} | |
# Prepare headers (Host excluded intentionally) | |
$headers = @{ | |
'x-ms-date' = $date | |
'x-ms-content-sha256' = $contentHash | |
'x-ms-nonce' = $Nonce | |
'x-authorization' = "HMAC-$($Algorithm.Replace('HMAC','')) SignedHeaders=$signedHeaders&Signature=$signature" | |
} | |
return $headers | |
} | |
function Test-HmacAuthorization { | |
<# | |
.SYNOPSIS | |
Verifies HMAC signature of incoming Azure Automation webhook requests. | |
.DESCRIPTION | |
Validates HMAC signature based on signed request headers (timestamp, nonce, content hash) and a shared secret. | |
Supports both HMACSHA256 and HMACSHA512 algorithms. | |
The shared secret is passed securely as a SecureString, converted as late as possible, and cleared from memory immediately after use. | |
Signature verification includes: | |
- Timestamp freshness validation (anti-replay window configurable via AllowedTimeDriftMinutes) | |
- Host | |
- Body content hash integrity check | |
- Nonce inclusion for replay protection (future extension to store/check nonce is left open) | |
.EXAMPLE | |
# ✅ Example 1: Production (Azure Automation) | |
# Retrieve encrypted variable securely | |
$sharedSecret = (Get-AutomationVariable -Name "SharedSecret") | ConvertTo-SecureString -AsPlainText -Force | |
# Verify signature | |
if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) { | |
[void]$sharedSecret.Dispose() | |
throw "Unauthorized: Signature verification failed." | |
} | |
# Dispose secret after use | |
[void]$sharedSecret.Dispose() | |
.EXAMPLE | |
# ✅ Example 2: Local Development / Testing | |
# Use test shared secret (DO NOT use hardcoded secrets in production) | |
$sharedSecret = ConvertTo-SecureString -String "MyTestSecretKey" -AsPlainText -Force | |
# Verify signature | |
if (-not (Test-HmacAuthorization -SharedSecret $sharedSecret -WebhookData $WebhookData)) { | |
Write-Output "❌ Signature invalid." | |
} else { | |
Write-Output "✅ Signature valid." | |
} | |
# Dispose secret | |
[void]$sharedSecret.Dispose() | |
.FUNCTIONALITY | |
Security, HMAC Authentication | |
.NOTES | |
Author: Julian Pawlowski | |
Company Name: Workoho GmbH | |
Created: 2025-03-17 | |
Updated: 2025-03-18 | |
#> | |
param( | |
# The shared secret key used for HMAC calculation (SecureString) | |
[Parameter(Mandatory)][securestring]$SharedSecret, | |
# The full webhook request data object passed by Azure Automation (includes headers and body) | |
[Parameter(Mandatory)][object]$WebhookData, | |
# Allowed time difference in minutes for timestamp validation (default: 5 minutes) | |
[int]$AllowedTimeDriftMinutes = 5, | |
# Expected request path (used in canonical message construction) | |
[string]$ExpectedPath = '/webhooks' | |
) | |
$allowedAlgorithms = @('HMACSHA256', 'HMACSHA512') | |
$headers = $WebhookData.RequestHeader | |
$authHeader = $headers.'x-authorization' | |
if (-not $authHeader) { | |
Write-Error 'Missing x-authorization header' | |
return $false | |
} | |
if ($authHeader -notmatch '^HMAC-(?<Algorithm>[A-Z0-9]+)\s+SignedHeaders=(?<SignedHeaders>[^&]+)&Signature=(?<Signature>.+)$') { | |
Write-Error 'Invalid x-authorization header format' | |
return $false | |
} | |
$algorithm = "HMAC$($matches['Algorithm'])" | |
if ($allowedAlgorithms -notcontains $algorithm) { | |
Write-Error "Algorithm $algorithm not allowed" | |
return $false | |
} | |
$signedHeaders = $matches['SignedHeaders'].Split(';') | |
$receivedHmac = $matches['Signature'] | |
foreach ($header in $signedHeaders) { | |
if ([string]::IsNullOrEmpty($headers.$header)) { | |
Write-Error "Missing signed header: $header" | |
return $false | |
} | |
} | |
# Host header check | |
if ([string]::IsNullOrEmpty($headers.'Host')) { | |
Write-Error 'Host header required' | |
return $false | |
} | |
# Timestamp freshness check | |
if ([string]::IsNullOrEmpty($headers.'x-ms-date')) { | |
Write-Error 'x-ms-date header required' | |
return $false | |
} | |
else { | |
try { | |
$requestTime = [datetime]::Parse($headers.'x-ms-date').ToUniversalTime() | |
$currentTime = (Get-Date).ToUniversalTime() | |
if ([math]::Abs(($currentTime - $requestTime).TotalMinutes) -gt $AllowedTimeDriftMinutes) { | |
Write-Error 'Request timestamp expired' | |
return $false | |
} | |
} | |
catch { | |
Write-Error 'Invalid timestamp format' | |
return $false | |
} | |
} | |
# Body hash check | |
if ([string]::IsNullOrEmpty($headers.'x-ms-content-sha256')) { | |
Write-Error 'x-ms-content-sha256 header required' | |
return $false | |
} | |
else { | |
$bodyBytes = [Text.Encoding]::UTF8.GetBytes($WebhookData.RequestBody) | |
$computedBodyHash = [Convert]::ToBase64String(([System.Security.Cryptography.SHA256]::Create()).ComputeHash($bodyBytes)) | |
if ($headers.'x-ms-content-sha256' -ne $computedBodyHash) { | |
Write-Error 'Content hash mismatch' | |
return $false | |
} | |
} | |
# Nonce check | |
if ([string]::IsNullOrEmpty($headers.'x-ms-nonce')) { | |
Write-Error 'x-ms-nonce header required' | |
return $false | |
} | |
# Optional future: Implement nonce storage to prevent re-use attacks | |
# Canonical message construction | |
$method = 'POST' | |
$path = $ExpectedPath | |
$headerValues = foreach ($header in $signedHeaders) { $headers.$header } | |
$canonicalMessage = "$method`n$path`n" + ($headerValues -join ';') | |
$unsecureSecret = $null | |
try { | |
$hmac = New-Object ("System.Security.Cryptography.$algorithm") | |
# SecureString → plaintext | |
$unsecureSecret = [System.Net.NetworkCredential]::New("", $SharedSecret).Password | |
$hmac.Key = [Text.Encoding]::UTF8.GetBytes($unsecureSecret) | |
$computedHmac = [Convert]::ToBase64String($hmac.ComputeHash([Text.Encoding]::UTF8.GetBytes($canonicalMessage))) | |
# Cleanup HMAC key | |
[Array]::Clear($hmac.Key, 0, $hmac.Key.Length) | |
$hmac = $null | |
} | |
finally { | |
if ($unsecureSecret) { | |
[Array]::Clear($unsecureSecret.ToCharArray(), 0, $unsecureSecret.Length) | |
$unsecureSecret = $null | |
} | |
} | |
# Final comparison (constant-time) | |
return [System.Security.Cryptography.CryptographicOperations]::FixedTimeEquals( | |
[Text.Encoding]::UTF8.GetBytes($computedHmac), | |
[Text.Encoding]::UTF8.GetBytes($receivedHmac) | |
) | |
} | |
function Assert-ParameterType { | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$Name, | |
[Parameter(Mandatory = $true)] | |
$Value, | |
[Parameter(Mandatory = $true)] | |
[type[]]$ExpectedTypes | |
) | |
if ($null -eq $Value) { return $false } | |
# Check if the value type matches any of the expected types | |
$matchFound = $false | |
foreach ($type in $ExpectedTypes) { | |
if ($Value -is $type) { | |
$matchFound = $true | |
break | |
} | |
} | |
if (-not $matchFound) { | |
$typeNames = $ExpectedTypes | ForEach-Object { "[$($_.Name)]" } | |
throw "Parameter '$Name' must be one of these types: $($typeNames -join ', '), but received [$($Value.GetType().Name)]" | |
} | |
return $true | |
} | |
function Invoke-ResilientRemoteCall { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory = $true)] | |
[scriptblock]$ScriptBlock | |
) | |
$maxRetries = 3 | |
$retryCount = 0 | |
$backoffInterval = 15 | |
do { | |
try { | |
return Invoke-Command -ScriptBlock $ScriptBlock | |
} | |
catch { | |
if ($retryCount -ge $maxRetries) { | |
Write-Verbose "Operation failed after $maxRetries attempts: $ScriptBlock" | |
throw | |
} | |
$retryCount++ | |
Write-Verbose "Operation failed, retrying in $backoffInterval seconds (Attempt $retryCount of $maxRetries)" | |
Start-Sleep -Seconds $backoffInterval | |
} | |
} while ($true) | |
} | |
function Get-OrganizationInfo { | |
[CmdletBinding()] | |
param() | |
try { | |
$organization = Invoke-ResilientRemoteCall { | |
Invoke-MgGraphRequest -Method GET -Uri "/v1.0/organization" -ErrorAction Stop | |
} | |
if (-not $organization -or -not $organization.value -or $organization.value.Count -eq 0) { | |
Write-Verbose "Could not retrieve organization information" | |
return $null | |
} | |
return $organization.value[0] | |
} | |
catch { | |
Write-Warning "Failed to retrieve tenant information: $_" | |
return $null | |
} | |
} | |
function Get-TenantBrandingImages { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory = $true)] | |
[string]$TenantId | |
) | |
try { | |
$brandingImages = @{} | |
# Get the branding configuration (contains URLs, not binary data) | |
try { | |
$branding = Invoke-ResilientRemoteCall { | |
Invoke-MgGraphRequest -Method GET ` | |
-Uri "/v1.0/organization/$TenantId/branding" ` | |
-ErrorAction Stop | |
} | |
if (-not $branding -or -not $branding.cdnList -or $branding.cdnList.Count -eq 0) { | |
Write-Verbose "No branding CDN information found" | |
return $null | |
} | |
# Use the first CDN domain | |
$cdnDomain = 'https://' + $branding.cdnList[0] + '/' | |
Write-Verbose "Using CDN domain: $cdnDomain" | |
# Process banner logo | |
if (-not [string]::IsNullOrEmpty($branding.bannerLogoRelativeUrl)) { | |
try { | |
$logoUrl = "$cdnDomain$($branding.bannerLogoRelativeUrl)" | |
Write-Verbose "Retrieving banner logo from: $logoUrl" | |
$response = Invoke-ResilientRemoteCall { | |
Invoke-WebRequest -Uri $logoUrl -ErrorAction Stop | |
} | |
if ($response.StatusCode -eq 200) { | |
# Get content type from response headers or determine from content | |
$contentType = $response.Headers['Content-Type'] | |
# Check if content type is generic or missing | |
if ([string]::IsNullOrEmpty($contentType) -or $contentType -eq 'image/*') { | |
$contentType = Get-ImageContentTypeFromBytes -ImageBytes $response.Content | |
Write-Verbose "Determined banner logo content type from binary data: $contentType" | |
} | |
$base64 = [System.Convert]::ToBase64String($response.Content) | |
$brandingImages["tenantBannerLogo"] = "data:$contentType;base64,$base64" | |
Write-Verbose "Retrieved tenant banner logo ($contentType)" | |
} | |
} | |
catch { | |
Write-Verbose "Failed to retrieve banner logo: $_" | |
} | |
} | |
# Process square logo (light) | |
if (-not [string]::IsNullOrEmpty($branding.squareLogoRelativeUrl)) { | |
try { | |
$logoUrl = "$cdnDomain$($branding.squareLogoRelativeUrl)" | |
Write-Verbose "Retrieving square logo from: $logoUrl" | |
$response = Invoke-ResilientRemoteCall { | |
Invoke-WebRequest -Uri $logoUrl -ErrorAction Stop | |
} | |
if ($response.StatusCode -eq 200) { | |
# Get content type from response headers or determine from content | |
$contentType = $response.Headers['Content-Type'] | |
# Check if content type is generic or missing | |
if ([string]::IsNullOrEmpty($contentType) -or $contentType -eq 'image/*') { | |
$contentType = Get-ImageContentTypeFromBytes -ImageBytes $response.Content | |
Write-Verbose "Determined square logo content type from binary data: $contentType" | |
} | |
$base64 = [System.Convert]::ToBase64String($response.Content) | |
$brandingImages["tenantSquareLogoLight"] = "data:$contentType;base64,$base64" | |
Write-Verbose "Retrieved tenant square logo (light) ($contentType)" | |
} | |
} | |
catch { | |
Write-Verbose "Failed to retrieve square logo: $_" | |
} | |
} | |
# Process square logo dark | |
if (-not [string]::IsNullOrEmpty($branding.squareLogoDarkRelativeUrl)) { | |
try { | |
$logoUrl = "$cdnDomain$($branding.squareLogoDarkRelativeUrl)" | |
Write-Verbose "Retrieving dark square logo from: $logoUrl" | |
$response = Invoke-ResilientRemoteCall { | |
Invoke-WebRequest -Uri $logoUrl -ErrorAction Stop | |
} | |
if ($response.StatusCode -eq 200) { | |
# Get content type from response headers or determine from content | |
$contentType = $response.Headers['Content-Type'] | |
# Check if content type is generic or missing | |
if ([string]::IsNullOrEmpty($contentType) -or $contentType -eq 'image/*') { | |
$contentType = Get-ImageContentTypeFromBytes -ImageBytes $response.Content | |
Write-Verbose "Determined dark square logo content type from binary data: $contentType" | |
} | |
$base64 = [System.Convert]::ToBase64String($response.Content) | |
$brandingImages["tenantSquareLogoDark"] = "data:$contentType;base64,$base64" | |
Write-Verbose "Retrieved tenant square logo (dark) ($contentType)" | |
} | |
} | |
catch { | |
Write-Verbose "Failed to retrieve dark square logo: $_" | |
} | |
} | |
} | |
catch { | |
Write-Verbose "Failed to retrieve organization branding: $_" | |
} | |
return $brandingImages | |
} | |
catch { | |
Write-Warning "Failed to retrieve tenant branding: $_" | |
return $null | |
} | |
} | |
function Get-ImageContentTypeFromBytes { | |
[CmdletBinding()] | |
[OutputType([string])] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[byte[]]$ImageBytes | |
) | |
# Check file signature (magic numbers) to determine file type | |
if ($ImageBytes.Length -gt 4) { | |
# JPEG: FF D8 FF | |
if ($ImageBytes[0] -eq 0xFF -and $ImageBytes[1] -eq 0xD8 -and $ImageBytes[2] -eq 0xFF) { | |
return "image/jpeg" | |
} | |
# PNG: 89 50 4E 47 | |
elseif ($ImageBytes[0] -eq 0x89 -and $ImageBytes[1] -eq 0x50 -and | |
$ImageBytes[2] -eq 0x4E -and $ImageBytes[3] -eq 0x47) { | |
return "image/png" | |
} | |
# GIF: 47 49 46 38 | |
elseif ($ImageBytes[0] -eq 0x47 -and $ImageBytes[1] -eq 0x49 -and | |
$ImageBytes[2] -eq 0x46 -and $ImageBytes[3] -eq 0x38) { | |
return "image/gif" | |
} | |
# BMP: 42 4D | |
elseif ($ImageBytes[0] -eq 0x42 -and $ImageBytes[1] -eq 0x4D) { | |
return "image/bmp" | |
} | |
# WebP: 52 49 46 46 xx xx xx xx 57 45 42 50 | |
elseif ($ImageBytes.Length -gt 11 -and | |
$ImageBytes[0] -eq 0x52 -and $ImageBytes[1] -eq 0x49 -and | |
$ImageBytes[2] -eq 0x46 -and $ImageBytes[3] -eq 0x46 -and | |
$ImageBytes[8] -eq 0x57 -and $ImageBytes[9] -eq 0x45 -and | |
$ImageBytes[10] -eq 0x42 -and $ImageBytes[11] -eq 0x50) { | |
return "image/webp" | |
} | |
# SVG: Check if it starts with <?xml or <svg | |
elseif ($ImageBytes.Length -gt 5) { | |
$possibleXml = [System.Text.Encoding]::ASCII.GetString($ImageBytes[0..5]) | |
if ($possibleXml -match '^<\?xml' -or $possibleXml -match '^<svg') { | |
return "image/svg+xml" | |
} | |
} | |
} | |
# Default to png if unknown | |
Write-Verbose "Could not determine image type from magic bytes, defaulting to PNG" | |
return "image/png" | |
} | |
function Expand-Template { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$Template, | |
[Parameter(Mandatory = $true)] | |
[hashtable]$Variables | |
) | |
$result = $Template | |
# Replace each variable placeholder with its value | |
foreach ($key in $Variables.Keys) { | |
# Match "{{" followed by any whitespace, then the variable name, then any whitespace, then "}}" | |
$placeholder = [regex]::new("\{\{\s*" + [regex]::Escape($key) + "\s*\}\}") | |
# Escape $ in replacement value to prevent regex substitution issues | |
$value = if ($null -eq $Variables[$key]) { "" } else { | |
[regex]::Replace($Variables[$key].ToString(), '[$]', '$$$$') | |
} | |
$result = $placeholder.Replace($result, $value) | |
} | |
# Look for any remaining {{ variable }} patterns that weren't replaced - using flexible whitespace | |
$unreplacedVariables = [regex]::Matches($result, '\{\{\s*[\w\.]+\s*\}\}') | |
if ($unreplacedVariables.Count -gt 0) { | |
# Extract just the variable names for clearer reporting | |
$variableNames = $unreplacedVariables | ForEach-Object { | |
$match = $_.Value -replace '\{\{\s*([\w\.]+)\s*\}\}', '$1' | |
$match | |
} | |
Write-Verbose "Warning: Found unreplaced template variables: $($variableNames -join ', ')" | |
} | |
return $result | |
} | |
function Send-EmailNotification { | |
[CmdletBinding( | |
SupportsShouldProcess, | |
ConfirmImpact = 'Medium' | |
)] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[ValidateSet( | |
'TAPCreated', | |
'UserBlocked', | |
'TAPNotEnabled', | |
'TAPExpiredWithOtherMethodsConfigured', | |
'TAPActiveWithOtherMethodsConfigured', | |
'OtherMethodsConfigured' | |
)] | |
[string]$Reason, | |
[string]$SenderEmailAddress, | |
[string]$EmailLanguage, | |
[string]$EmailSubject, | |
[string]$EmailTitle, | |
[string]$EmailSalutation, | |
[string]$EmailClosing, | |
[string]$EmailBodyText, | |
[string]$EmailTemplate, | |
[string]$EmailImagesJson, | |
[bool]$UseHtmlEmail, | |
[Parameter(Mandatory = $true)] | |
[object]$Data | |
) | |
$organizationInfo = Get-OrganizationInfo | |
$languageCode = if ([string]::IsNullOrEmpty($EmailLanguage)) { | |
if ([string]::IsNullOrEmpty($Data.Manager.PreferredLanguage)) { | |
'en' | |
} | |
else { | |
$Data.Manager.PreferredLanguage.Substring(0, 2).ToLower() | |
} | |
} | |
else { | |
$EmailLanguage.Substring(0, 2).ToLower() | |
} | |
Write-Verbose "Sending email in language: $languageCode" | |
$timeTranslations = @{ | |
en = @{ | |
'hour' = 'hour' | |
'hours' = 'hours' | |
'minute' = 'minute' | |
'minutes' = 'minutes' | |
} | |
de = @{ | |
'hour' = 'Stunde' | |
'hours' = 'Stunden' | |
'minute' = 'Minute' | |
'minutes' = 'Minuten' | |
} | |
fr = @{ | |
'hour' = 'heure' | |
'hours' = 'heures' | |
'minute' = 'minute' | |
'minutes' = 'minutes' | |
} | |
} | |
$hour = if ($timeTranslations[$languageCode]) { | |
$timeTranslations[$languageCode]['hour'] | |
} | |
else { | |
$timeTranslations['en']['hour'] | |
} | |
$hours = if ($timeTranslations[$languageCode]) { | |
$timeTranslations[$languageCode]['hours'] | |
} | |
else { | |
$timeTranslations['en']['hours'] | |
} | |
$minute = if ($timeTranslations[$languageCode]) { | |
$timeTranslations[$languageCode]['minute'] | |
} | |
else { | |
$timeTranslations['en']['minute'] | |
} | |
$minutes = if ($timeTranslations[$languageCode]) { | |
$timeTranslations[$languageCode]['minutes'] | |
} | |
else { | |
$timeTranslations['en']['minutes'] | |
} | |
# Build allowed variables dictionary for template expansion | |
$templateVariables = @{ | |
orgTenantId = $organizationInfo.Id | |
orgDisplayName = $organizationInfo.DisplayName | |
orgStreet = $organizationInfo.Street | |
orgPostalCode = $organizationInfo.PostalCode | |
orgCity = $organizationInfo.City | |
orgState = $organizationInfo.State | |
orgCountry = $organizationInfo.Country ?? $organizationInfo.CountryLetterCode | |
orgCountryLetterCode = $organizationInfo.CountryLetterCode | |
orgPrivacyContact = $organizationInfo.PrivacyProfile.ContactEmail | |
orgPrivacyStatementUrl = $organizationInfo.PrivacyProfile.StatementUrl | |
userDisplayName = $Data.DisplayName | |
userGivenName = $Data.GivenName ?? $Data.DisplayName | |
userSurname = $Data.Surname | |
userPrincipalName = $Data.UserPrincipalName | |
userMail = $Data.Mail | |
managerDisplayName = $Data.Manager.DisplayName | |
managerGivenName = $Data.Manager.GivenName ?? $Data.Manager.DisplayName | |
managerSurname = $Data.Manager.Surname | |
managerMail = $Data.Manager.Mail | |
temporaryAccessPass = $Data.TemporaryAccessPass.temporaryAccessPass | |
startTime = Get-Date $Data.TemporaryAccessPass.startDateTime -UFormat '%Y-%m-%d %R' | |
lifetimeInMinutes = $Data.TemporaryAccessPass.lifetimeInMinutes.ToString() | |
lifetimeInHours = [math]::Round($Data.TemporaryAccessPass.lifetimeInMinutes / 60, 0).ToString() | |
lifeTimeInHoursMinutes = [math]::Floor($Data.TemporaryAccessPass.lifetimeInMinutes / 60).ToString() + | |
$(if ([math]::Floor($Data.TemporaryAccessPass.lifetimeInMinutes / 60) -eq 1) { " $hour" } else { " $hours" }) + | |
$(if (($Data.TemporaryAccessPass.lifetimeInMinutes % 60) -gt 0) { | |
' ' + ($Data.TemporaryAccessPass.lifetimeInMinutes % 60).ToString() + | |
$(if (($Data.TemporaryAccessPass.lifetimeInMinutes % 60) -eq 1) { " $minute" } else { " $minutes" }) | |
} | |
else { '' }) | |
expirationTime = Get-Date (Get-Date $Data.TemporaryAccessPass.startDateTime).AddMinutes($Data.TemporaryAccessPass.lifetimeInMinutes) -UFormat '%Y-%m-%d %R' | |
} | |
$EmailSubjectTranslations = @{ | |
en = @{ | |
TAPCreated = '[CONFIDENTIAL] Temporary Access Pass for {{ userDisplayName }}' | |
UserBlocked = 'Blocked creation of Temporary Access Pass for {{ userDisplayName }}' | |
TAPNotEnabled = 'Feature not enabled to create Temporary Access Pass for {{ userDisplayName }}' | |
TAPExpiredWithOtherMethodsConfigured = 'Failed to renew Temporary Access Pass for {{ userDisplayName }}' | |
TAPActiveWithOtherMethodsConfigured = 'Failed to re-create Temporary Access Pass for {{ userDisplayName }}' | |
OtherMethodsConfigured = 'Failed to create Temporary Access Pass for {{ userDisplayName }}' | |
} | |
de = @{ | |
TAPCreated = '[VERTRAULICH] Befristeter Zugriffscode für {{ userDisplayName }}' | |
UserBlocked = 'Erstellung blockiert: Befristeter Zugriffscode für {{ userDisplayName }}' | |
TAPNotEnabled = 'Funktion nicht aktiviert: Befristeter Zugriffscode für {{ userDisplayName }}' | |
TAPExpiredWithOtherMethodsConfigured = 'Verlängerung fehlgeschlagen: Befristeter Zugriffscode für {{ userDisplayName }}' | |
TAPActiveWithOtherMethodsConfigured = 'Neuerstellung fehlgeschlagen: Befristeter Zugriffscode für {{ userDisplayName }}' | |
OtherMethodsConfigured = 'Erstellung fehlgeschlagen: Befristeter Zugriffscode für {{ userDisplayName }}' | |
} | |
fr = @{ | |
TAPCreated = "[CONFIDENTIEL] Code d’accès temporaire pour {{ userDisplayName }}" | |
UserBlocked = "Création du code d’accès temporaire bloquée pour {{ userDisplayName }}" | |
TAPNotEnabled = "Fonctionnalité non activée pour créer un code d’accès temporaire pour {{ userDisplayName }}" | |
TAPExpiredWithOtherMethodsConfigured = "Échec du renouvellement du code d’accès temporaire pour {{ userDisplayName }}" | |
TAPActiveWithOtherMethodsConfigured = "Échec de la recréation du code d’accès temporaire pour {{ userDisplayName }}" | |
OtherMethodsConfigured = "Échec de la création du code d’accès temporaire pour {{ userDisplayName }}" | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailSubject)) { | |
if ($EmailSubjectTranslations.ContainsKey($languageCode)) { | |
$EmailSubject = $EmailSubjectTranslations[$languageCode][$Reason] | |
} | |
else { | |
$EmailSubject = $EmailSubjectTranslations['en'][$Reason] | |
} | |
} | |
else { | |
try { | |
$EmailSubjects = ConvertFrom-Json -InputObject $EmailSubject -AsHashtable -ErrorAction Stop | |
if ($EmailSubjects -is [hashtable]) { | |
if ($EmailSubjects.ContainsKey($languageCode)) { | |
if ($EmailSubjects[$languageCode] -is [hashtable]) { | |
if ($EmailSubjects[$languageCode].ContainsKey($Reason)) { | |
$EmailSubject = $EmailSubjects[$languageCode][$Reason] | |
} | |
else { | |
$EmailSubject = $EmailSubjectTranslations['en'][$Reason] | |
} | |
} | |
else { | |
$EmailSubject = $EmailSubjects[$languageCode] | |
} | |
} | |
elseif ($EmailSubjects.ContainsKey('en')) { | |
if ($EmailSubjects['en'] -is [hashtable]) { | |
if ($EmailSubjects['en'].ContainsKey($Reason)) { | |
$EmailSubject = $EmailSubjects['en'][$Reason] | |
} | |
else { | |
$EmailSubject = $EmailSubjectTranslations['en'][$Reason] | |
} | |
} | |
else { | |
$EmailSubject = $EmailSubjects['en'] | |
} | |
} | |
else { | |
$EmailSubject = $EmailSubjectTranslations['en'][$Reason] | |
} | |
} | |
} | |
catch { | |
Write-Verbose 'EmailSubject: Not a valid JSON object found, interpreting as raw text' | |
} | |
} | |
$templateVariables.emailSubject = Expand-Template -Template $EmailSubject -Variables $templateVariables | |
$EmailTitleTranslations = @{ | |
en = @{ | |
TAPCreated = 'Onboarding Temporary Access Pass Code' | |
UserBlocked = 'Creation of Temporary Access Pass blocked' | |
TAPNotEnabled = 'Usage of Temporary Access Pass not enabled' | |
TAPExpiredWithOtherMethodsConfigured = 'Renewal of Temporary Access Pass not possible' | |
TAPActiveWithOtherMethodsConfigured = 'Re-creation of Temporary Access Pass not possible' | |
OtherMethodsConfigured = 'Creation of Temporary Access Pass not possible' | |
} | |
de = @{ | |
TAPCreated = 'Befristeter Zugriffscode für den Onboarding-Prozess' | |
UserBlocked = 'Erstellung des befristeten Zugriffscodes blockiert' | |
TAPNotEnabled = 'Nutzung des befristeten Zugriffscodes nicht freigeschaltet' | |
TAPExpiredWithOtherMethodsConfigured = 'Verlängerung des befristeten Zugriffscodes nicht möglich' | |
TAPActiveWithOtherMethodsConfigured = 'Neuerstellung des befristeten Zugriffscodes nicht möglich' | |
OtherMethodsConfigured = 'Erstellung des befristeten Zugriffscodes nicht möglich' | |
} | |
fr = @{ | |
TAPCreated = "Code d’accès temporaire pour l’intégration" | |
UserBlocked = "Création du code d’accès temporaire bloquée" | |
TAPNotEnabled = "Utilisation du code d’accès temporaire non activée" | |
TAPExpiredWithOtherMethodsConfigured = "Renouvellement du code d’accès temporaire impossible" | |
TAPActiveWithOtherMethodsConfigured = "Recréation du code d’accès temporaire impossible" | |
OtherMethodsConfigured = "Création du code d’accès temporaire impossible" | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailTitle)) { | |
if ($EmailTitleTranslations.ContainsKey($languageCode)) { | |
$EmailTitle = $EmailTitleTranslations[$languageCode][$Reason] | |
} | |
else { | |
$EmailTitle = $EmailTitleTranslations['en'][$Reason] | |
} | |
} | |
else { | |
try { | |
$EmailTitles = ConvertFrom-Json -InputObject $EmailTitle -AsHashtable -ErrorAction Stop | |
if ($EmailTitles -is [hashtable]) { | |
if ($EmailTitles.ContainsKey($languageCode)) { | |
if ($EmailTitles[$languageCode] -is [hashtable]) { | |
if ($EmailTitles[$languageCode].ContainsKey($Reason)) { | |
$EmailTitle = $EmailTitles[$languageCode][$Reason] | |
} | |
else { | |
$EmailTitle = $EmailTitleTranslations['en'][$Reason] | |
} | |
} | |
else { | |
$EmailTitle = $EmailTitles[$languageCode] | |
} | |
} | |
elseif ($EmailTitles.ContainsKey('en')) { | |
if ($EmailTitles['en'] -is [hashtable]) { | |
if ($EmailTitles['en'].ContainsKey($Reason)) { | |
$EmailTitle = $EmailTitles['en'][$Reason] | |
} | |
else { | |
$EmailTitle = $EmailTitleTranslations['en'][$Reason] | |
} | |
} | |
else { | |
$EmailTitle = $EmailTitles['en'] | |
} | |
} | |
else { | |
$EmailTitle = $EmailTitleTranslations['en'][$Reason] | |
} | |
} | |
} | |
catch { | |
Write-Verbose 'EmailTitle: Not a valid JSON object found, interpreting as raw text' | |
} | |
} | |
$templateVariables.emailTitle = Expand-Template -Template $EmailTitle -Variables $templateVariables | |
$EmailSalutationTranslations = @{ | |
en = 'Dear {{ managerGivenName }} {{ managerSurname }},' | |
de = 'Hallo {{ managerGivenName }} {{ managerSurname }},' | |
fr = 'Bonjour {{ managerGivenName }} {{ managerSurname }},' | |
} | |
if ([string]::IsNullOrEmpty($EmailSalutation)) { | |
if ($EmailSalutationTranslations.ContainsKey($languageCode)) { | |
$EmailSalutation = $EmailSalutationTranslations[$languageCode] | |
} | |
else { | |
$EmailSalutation = $EmailSalutationTranslations['en'] | |
} | |
} | |
else { | |
try { | |
$EmailSalutations = ConvertFrom-Json -InputObject $EmailSalutation -AsHashtable -ErrorAction Stop | |
if ($EmailSalutations -is [hashtable]) { | |
if ($EmailSalutations.ContainsKey($languageCode)) { | |
$EmailSalutation = $EmailSalutations[$languageCode] | |
} | |
elseif ($EmailSalutations.ContainsKey('en')) { | |
$EmailSalutation = $EmailSalutations['en'] | |
} | |
} | |
} | |
catch { | |
Write-Verbose 'EmailSalutation: Not a valid JSON object found, interpreting as raw text' | |
} | |
} | |
$templateVariables.emailSalutation = Expand-Template -Template $EmailSalutation -Variables $templateVariables | |
$EmailClosingTranslations = @{ | |
en = "Sincerely,`nYour {{ orgDisplayName }} IT Team" | |
de = "Freundliche Grüße`nIhr {{ orgDisplayName }} IT-Team" | |
fr = "Cordialement,`nL’équipe informatique de {{ orgDisplayName }}" | |
} | |
if ([string]::IsNullOrEmpty($EmailClosing)) { | |
if ($EmailClosingTranslations.ContainsKey($languageCode)) { | |
$EmailClosing = $EmailClosingTranslations[$languageCode] | |
} | |
else { | |
$EmailClosing = $EmailClosingTranslations['en'] | |
} | |
} | |
else { | |
try { | |
$EmailClosings = ConvertFrom-Json -InputObject $EmailClosing -AsHashtable -ErrorAction Stop | |
if ($EmailClosings -is [hashtable]) { | |
if ($EmailClosings.ContainsKey($languageCode)) { | |
$EmailClosing = $EmailClosings[$languageCode] | |
} | |
elseif ($EmailClosings.ContainsKey('en')) { | |
$EmailClosing = $EmailClosings['en'] | |
} | |
} | |
} | |
catch { | |
Write-Verbose 'EmailClosing: Not a valid JSON object found, interpreting as raw text' | |
} | |
} | |
$templateVariables.emailClosing = Expand-Template -Template $EmailClosing -Variables $templateVariables | |
#region HTML Email | |
if ($UseHtmlEmail) { | |
# Replace newlines with HTML line breaks | |
$templateVariables.emailSubject = $templateVariables.emailSubject -replace "`n", "<br>" | |
$templateVariables.emailTitle = $templateVariables.emailTitle -replace "`n", "<br>" | |
$templateVariables.emailSalutation = $templateVariables.emailSalutation -replace "`n", "<br>" | |
$templateVariables.emailClosing = $templateVariables.emailClosing -replace "`n", "<br>" | |
# Process email images from JSON if provided | |
$EmailImages = @{} | |
if (-not [string]::IsNullOrEmpty($EmailImagesJson)) { | |
try { | |
$parsedImages = ConvertFrom-Json -InputObject $EmailImagesJson -AsHashtable -ErrorAction Stop | |
if ($parsedImages -is [hashtable] -or $parsedImages -is [System.Collections.IDictionary]) { | |
$EmailImages = $parsedImages | |
} | |
} | |
catch { | |
Write-Warning "Failed to parse EmailImagesJson parameter: $_" | |
} | |
} | |
# Default email images | |
$EmailImages.iconWarning = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFcklEQVR4nO2Ve2iVZRzHD4Gey/uendt7WmzSQovFduYfpu4+z5xzkUSE3TAyDLKQ8A8hCsp/imJmQUaRgww0RPA6NdfOzs5lOzfPNndeaARGRSAp4khkMvHsPO833ufcb9vznq25Yj/4wGBjfL7f9/c8j0q1PMuzPAs2gOohiNanIVp/gChMQLTeoYwLE4gIxyBau+S/US3FgWhtgShEIFpBieQiAOMylisYtzaplsoguEoLUehGxErypa0J6RzGBAlXhB5MWPkHKy9ankVE+INJ+koSS5oxyzWMmncurvSYagVE4WVEhEDhFZlTOp8Rsw9h00vy/17Y1YiYKyGW2xCxtEMU9kEUehERJktsO5tRcz5h8y2MmM4hbNqHUYMdIyYbguZK2UWZvPhwIHUYZz2QStsuID2SxJQmnI102Qhy2eBTEKCQNGPbY4xt54qHC4tnwh5gHisijVow5TPjltOYxdSwCVKYrW0pl1AcBQFKP5DRkBm9h9Zg2zNd6OrqQmdnJ7q6OnDxqypEg+xtS6FMDBT2APNYkftBM777uJbKb926lQbo6OjA9x89gWiAvW0pIS0F07AHmMeBjAbNOPpJdUp+y5YtNMDx7jU0AJN0KFucElAUoPQDKa/Jic8ez5LfvHkzTn3+GKKB2VdEChYSL0vBHqCUWySx2zNBE05/sTpLvr29Hee/fDQeQKG0lMSvOMBs0sWvPznAhUNVVDwpb7fb0f91JWZogOIrIhWQTqNXEGAed/ZM0Ij+b1alxGU2bdoEb88jmPGzty0lpCm+OAoCmO6xvpC5yC27D1ekxGXa2toQOlKOmJ9FuixPXIYM66fZA4yYbih5ITMPZCxgQOBIOZWWaW1tpYwfExDzKZOWkgzzIEP8X+wBwsawkhcyc6/llseOWlPiLS0tlF9OmEH8s6+IlCOdCRnmgkoC9Ch5ITN3Wg7w83ELlW5ubqY0NTXht1MGEN8c4sP54tJQHOLlDisIYNqh5IXMPJByy1dPGql0ksbGRlzrLWNuWxrKhKPAw73CHmDMZCAh4322FzJ7r+UAf541UOmGhgZKY0M9bl7kmduWkuLeOMTDReExGFVKhoSM5xQ9NhkH8voFPerr6ykbN25EW8t63LrEM7ctJcTTAXRnFMnTrxAwtpV6Z0/28di1vQb21nWUPTuq8XdfMXGuoDTFo6PArWlWHIB+hYDBz/JC5u521KunwpOXOMrtPh4zXra2JU9aXIa4dcMlydOvEDSsI/4yovTOjnl53HUmAvzIYcrBI+Zha1tK4qbyBE7thpID0K/g1/fMLZ3ebVn+xgUe+3dXwd5sg72lDh++WYUb53jE3LO3LSXEkxC35tt5ydOv4CjniJ//lfX6m3byOLB3FdauXZuirq4O3e9UYnpgbmnJraUQl/Z3+FX6eQegIQJlG4hPf6+gdM6BvPMTh13Pr6HSMjabjfL6c6tx59Ls0pIrDhnUTsOjXb8g8qkQPt1OlutPbvng3grU1tZSampqKJ/uqcC0o4C4KxsyqJXg1Ly6oPKpEEP8/rnvbA43ezl88EYlGtdXo+Gparz7WiWun+ZAXPltS5kMagCn5n3VvznEyx0sfIukdzvm1uFuvw6T5+NM9ekQcxWT1lJxGeJUH1AtxsCre4/lFinadoa0lJbvXhT5VAiP9m3i0RFm6cSKZOGUxTUxDKrfWlT5jBAvELfubrEDWaxtWZzKD2im4Fy5/YHIp0OsfJIMaiZYpeOoQQbUV+FYYVMthZEfHOLSnmQRlwao/An0LtAjtZADl/ZFMqiezJNOijvUt+FQ71Yt5cGAtoI41WeT0kmIQ31G/p3qvzLoV3eSAfWEjPzzg/ZZnuX5v84/V/4B6rP1Gg4AAAAASUVORK5CYII=' | |
$EmailImages.iconInfo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFCUlEQVR4nO2YSUwbVxjHh1Ztis0SttisxmYxOxjUVL300kMvPfVS9dBL21SqVKlS1ahVKwW1YicYs+9hM5iYQIMDDmCsSlVTtRV7IDDeVzCZYcapoqAoKl81PtpRB88brB74S3Md/X5v3vfm+x6GXeQiF+Elb/5yJK5YPv6ofIm6WbZILZTpj1dLF0hL8QJpLZ4nV4t1xP0iHdFaqCM+zrtHpmP/h1TpvILyZfqawkD/VrF0fFq+REH5IgVl94+hVH8MJQsklMyTUHyPgCIdAYVzBBTefQwFPz8G+ezRXwV3jr6U3yViIw5epIXXK430dcUKdaQw0FCxTEGY8CCfOYL8O0eQN+2jcrW+GxlaV3RE4BWGJ28rVug9xQoNPMBDrtYHubcPQaY5sMk0vnfPFb7SSH+jWKFf8A2fM8UIHIJs0vuPVO39EQOI4pccIKrS6G+pNPrh/OAPQDpxANlqL0jGPeqqvtXXeOOvMvpVkYP3gmTMC1kjbg1WDa/wAf9dxOFHPZA54oGMYVcDErzCSL3Ddc/LtW7IU5shb9wMeRoXB3g3ZAy5TtOGPO9zgs/Vw6VKI41zgS/S2ED9iADq5EXgGd8hIH/cEh78LTekD7khbdDlkwzbL4ctULniv8Fp5ac9UPPHAQTnpwdekKld4cJD2oALUvsdyrDgr+rJOIWBorns+fwpJ0w+IkMEJnYIkI7aOcA7QdTrPEnqcKadWUBhoL/mWrD5Wi9cW3aECHyit4N03BU2vLgvIACibkfNmQUqluktlNMmR22Dz5cccBcnA8+nDPywhTt8jwNSuhzOMx2rZYYn+bwclRoPyEZtIB2xgUztRoK/0s0I2CGx0/YW++obqC9Q4XPGrJA9sAuS/l2Q9O1AVu8OSIbMSPDJnXZIbrd+z/4FFo+HUX9SDHxwMrsfosF32CCxzTp3BgHqd9Q/LLPqIQJd20jwSe02SFBZ99gF9KQTtT2Q9D4MEcjo3EKCT2xjBCxPWQVK9SSF2ttk9YQKpHdsIcEnqqyQoLScYlp49T8FiucJP2pjltm9HSKQ1rGJBt9qhctKC2BsbXbJPOlF7SpfKtC+iQwf32I+Yd1CxTpiA7UlZgo2OKltm4jwFohrNjtZBYrmiCnUfp4p2OCIVRtI8PHNFohtNC+xChTMEd+iDiNMwYYItG4gwcc1mRmBRvYvMEteRZ2kmIINjki5jgoPggbze6wCzDEln/V5uI+B7kDBBudKyzoSvLAB92PV9jfYBTAMk8/4GrnCM0clU7DBSbm5zhk+psEEwnp8EDtrZFpvVu607zkXeOaoZAo2RKB5jTN8TB1+KqzdL8PCSc5t3y2OM2ygYIOT3LTGDb7eBMJa0ywWbqSTPlGO5pDmMsOKlKECSU3rHOHxZ5fqcVnYAgGJCd9nnGbYjr3Anme2DbPySY1rkKTcDR++zgSCGvw6hpJs9cEk6iTFaeXrTBBds7+AfE/KXHtLxjy/Rhy+Ft+Or+ZwH/SyMBdLmSPuBxFc+U1B044Y4zOpfV5BxrBrJgLwSwkN1njsXAIQlT7o/ip1wPnsHOCfC2rwH3i5kWaLqN8hFfc5Z8W9jlN+4PHFmNr9AizSSemxlYt67GMp3fan4cPjJzH1pqnoOpz9rufcRTp3YhK77B8kt9s6k9psfyaqbP7QYcTyd2yjaTW2wdQnrNv/8Pz2OU8RNR8KhSqrSKB0pDKCfL33IhfB+M2/hW3M5uDgqEwAAAAASUVORK5CYII=' | |
# Get tenant branding images | |
$tenantBranding = Get-TenantBrandingImages -TenantId $organizationInfo.Id | |
if ($tenantBranding -and $tenantBranding.Count -gt 0) { | |
Write-Verbose "Retrieved tenant branding with $($tenantBranding.Count) images" | |
# Add tenant branding images to the EmailImages collection | |
foreach ($key in $tenantBranding.Keys) { | |
if (-not $EmailImages.ContainsKey($key)) { | |
$EmailImages[$key] = $tenantBranding[$key] | |
Write-Verbose "Added tenant branding image: $key" | |
} | |
} | |
} | |
# Add all image references to template variables | |
if ($EmailImages.Count -gt 0) { | |
# First add all possible image references to template variables | |
foreach ($imageKey in $EmailImages.Keys) { | |
$templateVariables["image:$imageKey"] = "cid:$imageKey" | |
} | |
} | |
$bodyContentType = 'HTML' | |
$EmailBodyAlertBannerTranslations = @{ | |
en = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#FFF4B5" style="min-width:600px; background-color:#FFF4B5; border-bottom:1px solid #e6c949;"> | |
<tr> | |
<td align="center" style="padding:8px 0; font-family:Arial, sans-serif; font-size:13px; color:#222222;"> | |
<img src="{{ image:iconWarning }}" alt="Warning" width="16" height="16" style="vertical-align:middle; margin-right:5px;" /> | |
<strong>Security notice:</strong> This message contains sensitive account information. Please handle with care. | |
</td> | |
</tr> | |
</table> | |
'@ | |
de = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#FFF4B5" style="min-width:600px; background-color:#FFF4B5; border-bottom:1px solid #e6c949;"> | |
<tr> | |
<td align="center" style="padding:8px 0; font-family:Arial, sans-serif; font-size:13px; color:#222222;"> | |
<img src="{{ image:iconWarning }}" alt="Warning" width="16" height="16" style="vertical-align:middle; margin-right:5px;" /> | |
<strong>Sicherheitshinweis:</strong> Diese Nachricht enthält sensible Kontoinformationen. Bitte sorgfältig behandeln. | |
</td> | |
</tr> | |
</table> | |
'@ | |
fr = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" bgcolor="#FFF4B5" style="min-width:600px; background-color:#FFF4B5; border-bottom:1px solid #e6c949;"> | |
<tr> | |
<td align="center" style="padding:8px 0; font-family:Arial, sans-serif; font-size:13px; color:#222222;"> | |
<img src="{{ image:iconWarning }}" alt="Warning" width="16" height="16" style="vertical-align:middle; margin-right:5px;" /> | |
<strong>Notification de sécurité :</strong> Ce message contient des informations sensibles concernant un compte. Veuillez le manipuler avec précaution. | |
</td> | |
</tr> | |
</table> | |
'@ | |
} | |
if ($Reason -ne 'TAPCreated') { | |
$templateVariables.emailBodyAlertBanner = '' | |
} | |
else { | |
$templateVariables.emailBodyAlertBanner = if ($EmailBodyAlertBannerTranslations.ContainsKey($languageCode)) { | |
$EmailBodyAlertBannerTranslations[$languageCode] | |
} | |
else { | |
$EmailBodyAlertBannerTranslations['en'] | |
} | |
} | |
$EmailBodyFooterHintTranslations = @{ | |
en = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px; margin-top:20px; border-top:1px dotted #ddd; font-size:11px; color:#666;"> | |
<p style="margin:5px 0;">This is an automated email containing a secure access code. For security reasons, the code will expire after use or after the time indicated above.</p> | |
</td> | |
</tr> | |
</table> | |
'@ | |
de = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px; margin-top:20px; border-top:1px dotted #ddd; font-size:11px; color:#666;"> | |
<p style="margin:5px 0;">Diese E-Mail wurde automatisch erstellt und enthält einen vorübergehenden Zugriffscode. Aus Sicherheitsgründen läuft der Code nach der Verwendung oder nach der oben angegebenen Zeit ab.</p> | |
</td> | |
</tr> | |
</table> | |
'@ | |
fr = @" | |
<table role='presentation' width='100%' border='0' cellspacing='0' cellpadding='0'> | |
<tr> | |
<td style='padding-top:10px; margin-top:20px; border-top:1px dotted #ddd; font-size:11px; color:#666;'> | |
<p style='margin:5px 0;'>Ceci est un e-mail automatique contenant un code d’accès temporaire. Pour des raisons de sécurité, le code expirera après son utilisation ou après l’heure d’expiration indiquée ci-dessus.</p> | |
</td> | |
</tr> | |
</table> | |
"@ | |
} | |
if ($Reason -ne 'TAPCreated') { | |
$templateVariables.emailBodyFooterHint = '' | |
} | |
else { | |
$templateVariables.emailBodyFooterHint = if ($EmailBodyFooterHintTranslations.ContainsKey($languageCode)) { | |
$EmailBodyFooterHintTranslations[$languageCode] | |
} | |
else { | |
$EmailBodyFooterHintTranslations['en'] | |
} | |
} | |
$EmailBodyTextTranslations = @{ | |
en = @{ | |
TAPCreated = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">A Temporary Access Pass code for onboarding has been created for the following person:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Code section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Please provide the following code to {{ userGivenName }}:</p> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="15" bgcolor="#e0e0e0" style="background-color:#e0e0e0; margin:15px 0; border:1px solid #ccc; border-radius:5px;"> | |
<tr> | |
<td align="center" style="font-family:monospace; font-size:18px; font-weight:bold; letter-spacing:2px;"> | |
{{ temporaryAccessPass }} | |
</td> | |
</tr> | |
</table> | |
<p style="margin:10px 0;">The code will expire in <strong>{{ lifeTimeInHoursMinutes }}</strong>, at <span style="color:#cc0000; font-weight:500;">{{ expirationTime }} UTC</span>.</p> | |
<p style="margin:10px 0;">Next steps for {{ userGivenName }}:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Use the code to sign in when prompted.</li> | |
<li style="margin-bottom:8px;">Set up the Microsoft Authenticator app or another authentication method.</li> | |
<li style="margin-bottom:8px;">After setup is complete, the Temporary Access Pass can be deleted at <a href="https://aka.ms/MySecurityInfo" style="color:#0066cc; text-decoration:underline; font-weight:500;">https://aka.ms/MySecurityInfo</a> .</li> | |
<li style="margin-bottom:8px;">From now on, use the new authentication methods you have set up.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
UserBlocked = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">A Temporary Access Pass code could not be created for the following person:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">The creation was blocked because this type of user account is not allowed to use this process to create a Temporary Access Pass code.</p> | |
<p style="margin:10px 0;">Next steps if {{ userGivenName }} is not able to sign in:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.</li> | |
<li style="margin-bottom:8px;">The support team will validate the request and provide guidance on how to proceed.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPNotEnabled = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">A Temporary Access Pass code could not be created for the following person:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">The creation was aborted because the Temporary Access Pass feature is not enabled for this user account.</p> | |
<p style="margin:10px 0;">Next steps if {{ userGivenName }} is not able to sign in:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.</li> | |
<li style="margin-bottom:8px;">The support team will validate the request and provide guidance on how to proceed.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPExpiredWithOtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">A Temporary Access Pass code could not be created for the following person:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured.</p> | |
<p>The previously issued Temporary Access Pass is no longer valid.</p> | |
<p style="margin:10px 0;">Please ask {{ userGivenName }} to sign in using their existing authentication methods.</p> | |
<p style="margin:10px 0;">Next steps if {{ userGivenName }} is not able to sign in:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.</li> | |
<li style="margin-bottom:8px;">The support team will validate the request and provide guidance on how to proceed.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPActiveWithOtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">A Temporary Access Pass code could not be created for the following person:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured.</p> | |
<p>{{ userGivenName }} must use the existing Temporary Access Pass to sign in before being able to use the other authentication methods.</p> | |
<p style="margin:10px 0;">If {{ userGivenName }} is able to sign in using the active Temporary Access Pass, they can remove it themselves at <a href="https://aka.ms/MySecurityInfo" style="color:#0066cc; text-decoration:underline; font-weight:500;">https://aka.ms/MySecurityInfo</a> to start using their other authentication methods.</p> | |
<p style="margin:10px 0;">Next steps if {{ userGivenName }} is not able to sign in:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Ask {{ userGivenName }} to contact IT support and request removal of the active Temporary Access Pass, or wait until the code expires.</li> | |
<li style="margin-bottom:8px;">{{ userGivenName }} may then try to sign in using one of their existing authentication methods.</li> | |
<li style="margin-bottom:8px;">The support team will provide further guidance to restore access to the account if needed.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
OtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">A Temporary Access Pass code could not be created for the following person:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">The creation was aborted because {{ userGivenName }} has already completed the onboarding process and already has authentication methods configured.</p> | |
<p>Temporary Access Pass codes must no longer be issued via the self-service process and require handling by IT support.</p> | |
<p style="margin:10px 0;">Please ask {{ userGivenName }} to sign in using their existing authentication methods.</p> | |
<p style="margin:10px 0;">Next steps if {{ userGivenName }} is not able to sign in:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods.</li> | |
<li style="margin-bottom:8px;">The support team will validate the request and provide guidance on how to proceed.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
} | |
de = @{ | |
TAPCreated = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Ein befristeter Zugriffscode für den Onboarding-Prozess wurde für die folgende Person erstellt:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Code section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Bitte geben Sie den folgenden Code an {{ userGivenName }} weiter:</p> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="15" bgcolor="#e0e0e0" style="background-color:#e0e0e0; margin:15px 0; border:1px solid #ccc; border-radius:5px;"> | |
<tr> | |
<td align="center" style="font-family:monospace; font-size:18px; font-weight:bold; letter-spacing:2px;"> | |
{{ temporaryAccessPass }} | |
</td> | |
</tr> | |
</table> | |
<p style="margin:10px 0;">Der Code läuft in <strong>{{ lifeTimeInHoursMinutes }}</strong> um <span style="color:#cc0000; font-weight:500;">{{ expirationTime }} (GMT)</span> ab.</p> | |
<p style="margin:10px 0;">Nächste Schritte für {{ userGivenName }}:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Verwenden Sie den Code, um sich anzumelden, wenn Sie dazu aufgefordert werden.</li> | |
<li style="margin-bottom:8px;">Richten Sie die Microsoft Authenticator-App oder eine andere Authentifizierungsmethode ein.</li> | |
<li style="margin-bottom:8px;">Nach Abschluss der Einrichtung kann der befristete Zugriffscode unter <a href="https://aka.ms/MySecurityInfo" style="color:#0066cc; text-decoration:underline; font-weight:500;">https://aka.ms/MySecurityInfo</a> gelöscht werden.</li> | |
<li style="margin-bottom:8px;">Nutzen Sie von nun an die neuen Authentifizierungsmethoden, die Sie eingerichtet haben.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
UserBlocked = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Die Erstellung wurde blockiert, da dieser Kontotyp nicht berechtigt ist, über diesen Prozess einen befristeten Zugriffscode zu erhalten.</p> | |
<p style="margin:10px 0;">Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.</li> | |
<li style="margin-bottom:8px;">Der Support prüft die Anfrage und hilft bei den nächsten Schritten.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPNotEnabled = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Die Erstellung wurde abgebrochen, da die Funktion für befristete Zugriffscodes für dieses Konto nicht aktiviert ist.</p> | |
<p style="margin:10px 0;">Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.</li> | |
<li style="margin-bottom:8px;">Der Support prüft die Anfrage und hilft bei den nächsten Schritten.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPExpiredWithOtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind.</p> | |
<p>Der zuvor ausgestellte Zugriffscode ist nicht mehr gültig.</p> | |
<p style="margin:10px 0;">Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden.</p> | |
<p style="margin:10px 0;">Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.</li> | |
<li style="margin-bottom:8px;">Der Support prüft die Anfrage und hilft bei den nächsten Schritten.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPActiveWithOtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind.</p> | |
<p>{{ userGivenName }} muss den bestehenden befristeten Zugriffscode verwenden, um sich anzumelden, bevor andere Authentifizierungsmethoden genutzt werden können.</p> | |
<p style="margin:10px 0;">Kann sich {{ userGivenName }} erfolgreich mit dem aktiven Zugriffscode anmelden, kann dieser unter <a href="https://aka.ms/MySecurityInfo" style="color:#0066cc; text-decoration:underline; font-weight:500;">https://aka.ms/MySecurityInfo</a> eigenständig gelöscht werden, um anschließend die anderen Authentifizierungsmethoden zu verwenden.</p> | |
<p style="margin:10px 0;">Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und die Entfernung des aktiven Zugriffscodes anzufordern, oder warten Sie die Ablaufzeit ab.</li> | |
<li style="margin-bottom:8px;">Anschließend kann {{ userGivenName }} versuchen, sich mit einer bestehenden Authentifizierungsmethode anzumelden.</li> | |
<li style="margin-bottom:8px;">Falls nötig, unterstützt der Support bei der Wiederherstellung des Kontozugriffs.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
OtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden:</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind.</p> | |
<p>Befristete Zugriffscodes dürfen nach Abschluss des Onboardings nicht mehr über den Self-Service-Prozess erstellt werden und erfordern die Unterstützung durch den IT-Support.</p> | |
<p style="margin:10px 0;">Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden.</p> | |
<p style="margin:10px 0;">Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern.</li> | |
<li style="margin-bottom:8px;">Der Support prüft die Anfrage und hilft bei den nächsten Schritten.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
} | |
fr = @{ | |
TAPCreated = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Un code d’accès temporaire pour l’intégration a été créé pour la personne suivant :</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Code section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">Veuillez transmettre le code suivant à {{ userGivenName }} :</p> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="15" bgcolor="#e0e0e0" style="background-color:#e0e0e0; margin:15px 0; border:1px solid #ccc; border-radius:5px;"> | |
<tr> | |
<td align="center" style="font-family:monospace; font-size:18px; font-weight:bold; letter-spacing:2px;"> | |
{{ temporaryAccessPass }} | |
</td> | |
</tr> | |
</table> | |
<p style="margin:10px 0;">Le code expirera dans <strong>{{ lifeTimeInHoursMinutes }}</strong>, à <span style="color:#cc0000; font-weight:500;">{{ expirationTime }} UTC</span>.</p> | |
<p style="margin:10px 0;">Prochaines étapes pour {{ userGivenName }} :</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Utilisez le code pour vous connecter lorsque cela vous est demandé.</li> | |
<li style="margin-bottom:8px;">Configurez l’application Microsoft Authenticator ou une autre méthode d’authentification.</li> | |
<li style="margin-bottom:8px;">Une fois la configuration terminée, le code d’accès temporaire peut être supprimé via <a href="https://aka.ms/MySecurityInfo" style="color:#0066cc; text-decoration:underline; font-weight:500;">https://aka.ms/MySecurityInfo</a> .</li> | |
<li style="margin-bottom:8px;">Utilisez ensuite les nouvelles méthodes d’authentification que vous avez configurées.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
UserBlocked = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">La création a été bloquée car ce type de compte n’est pas autorisé à utiliser ce processus pour obtenir un code d’accès temporaire.</p> | |
<p style="margin:10px 0;">Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification.</li> | |
<li style="margin-bottom:8px;">L’équipe d’assistance validera la demande et fournira des instructions pour la suite.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPNotEnabled = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">La création a été annulée car la fonctionnalité de code d’accès temporaire n’est pas activée pour ce compte.</p> | |
<p style="margin:10px 0;">Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification.</li> | |
<li style="margin-bottom:8px;">L’équipe d’assistance validera la demande et fournira des instructions pour la suite.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPExpiredWithOtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées.</p> | |
<p>Le code d’accès temporaire précédemment émis n’est plus valide.</p> | |
<p style="margin:10px 0;">Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes.</p> | |
<p style="margin:10px 0;">Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification.</li> | |
<li style="margin-bottom:8px;">L’équipe d’assistance validera la demande et fournira des instructions pour la suite.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
TAPActiveWithOtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées.</p> | |
<p>{{ userGivenName }} doit utiliser le code d’accès temporaire existant pour se connecter avant de pouvoir utiliser d’autres méthodes d’authentification.</p> | |
<p style="margin:10px 0;">Si {{ userGivenName }} parvient à se connecter avec le code d’accès temporaire actif, il peut le supprimer lui-même sur <a href="https://aka.ms/MySecurityInfo" style="color:#0066cc; text-decoration:underline; font-weight:500;">https://aka.ms/MySecurityInfo</a> afin d’utiliser ses autres méthodes d’authentification.</p> | |
<p style="margin:10px 0;">Next steps if {{ userGivenName }} is not able to sign in:</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la suppression du code d’accès temporaire actif, ou d’attendre son expiration.</li> | |
<li style="margin-bottom:8px;">Ensuite, {{ userGivenName }} peut essayer de se connecter avec une autre méthode d’authentification existante.</li> | |
<li style="margin-bottom:8px;">Si nécessaire, l’assistance informatique aidera à restaurer l’accès au compte.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
OtherMethodsConfigured = @' | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:10px;"> | |
<p style="margin:10px 0;">{{ emailSalutation }}</p> | |
<p style="margin:10px 0;">Un code d’accès temporaire n’a pas pu être créé pour la personne suivante :</p> | |
</td> | |
</tr> | |
<!-- User info section --> | |
<tr> | |
<td align="center" style="padding:15px 0;"> | |
<table role="presentation" border="0" cellspacing="0" cellpadding="10" style="margin:0 auto;" width="80%"> | |
<tr> | |
<td align="center"> | |
<p style="margin:5px 0;"><strong>{{ userDisplayName }}</strong></p> | |
<p style="margin:5px 0;">{{ userPrincipalName }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Justification section --> | |
<tr> | |
<td> | |
<p style="margin:10px 0;">La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification sont déjà configurées. </p> | |
<p>Après l’intégration, les codes d’accès temporaires ne doivent plus être délivrés via le processus en libre-service et nécessitent l’intervention de l’assistance informatique.</p> | |
<p style="margin:10px 0;">Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes.</p> | |
<p style="margin:10px 0;">Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter :</p> | |
<ol style="padding-left:25px; margin-top:10px;"> | |
<li style="margin-bottom:8px;">Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification.</li> | |
<li style="margin-bottom:8px;">L’équipe d’assistance validera la demande et fournira des instructions pour la suite.</li> | |
</ol> | |
</td> | |
</tr> | |
</table> | |
'@ | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailBodyText)) { | |
if ($EmailBodyTextTranslations.ContainsKey($languageCode)) { | |
$EmailBodyText = $EmailBodyTextTranslations[$languageCode][$Reason] | |
} | |
else { | |
$EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] | |
} | |
} | |
else { | |
try { | |
$EmailBodyTexts = ConvertFrom-Json -InputObject $EmailBodyText -AsHashtable -ErrorAction Stop | |
if ($EmailBodyTexts -is [hashtable]) { | |
if ($EmailBodyTexts.ContainsKey($languageCode)) { | |
if ($EmailBodyTexts[$languageCode] -is [hashtable]) { | |
if ($EmailBodyTexts[$languageCode].ContainsKey($Reason)) { | |
$EmailBodyText = $EmailBodyTexts[$languageCode][$Reason] | |
} | |
else { | |
$EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] | |
} | |
} | |
else { | |
$EmailBodyText = $EmailBodyTexts[$languageCode] | |
} | |
} | |
elseif ($EmailBodyTexts.ContainsKey('en')) { | |
if ($EmailBodyTexts['en'] -is [hashtable]) { | |
if ($EmailBodyTexts['en'].ContainsKey($Reason)) { | |
$EmailBodyText = $EmailBodyTexts['en'][$Reason] | |
} | |
else { | |
$EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] | |
} | |
} | |
else { | |
$EmailBodyText = $EmailBodyTexts['en'] | |
} | |
} | |
else { | |
$EmailBodyText = $EmailBodyTextTranslations['en'][$Reason] | |
} | |
} | |
} | |
catch { | |
Write-Verbose 'EmailBodyText: Not a valid JSON object found, interpreting as raw text' | |
} | |
} | |
$templateVariables.emailBodyText = Expand-Template -Template $EmailBodyText -Variables $templateVariables | |
#region HTML email template | |
$EmailTemplateTranslations = @{ | |
#region EN template | |
en = @' | |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta name="x-apple-disable-message-reformatting" /> | |
<!--[if mso]> | |
<xml> | |
<o:OfficeDocumentSettings> | |
<o:AllowPNG/> | |
<o:PixelsPerInch>96</o:PixelsPerInch> | |
</o:OfficeDocumentSettings> | |
</xml> | |
<![endif]--> | |
<style type="text/css"> | |
/* Client-specific styles */ | |
#outlook a{padding:0;} | |
.ReadMsgBody{width:100%;} | |
.ExternalClass{width:100%;} | |
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div{line-height: 100%;} | |
<!--[if mso]> | |
body, table, td, div, p { | |
font-family: Arial, sans-serif !important; | |
} | |
table { | |
border-collapse: collapse !important; | |
mso-table-lspace: 0pt !important; | |
mso-table-rspace: 0pt !important; | |
} | |
<![endif]--> | |
/* Reset styles */ | |
html, body { | |
margin: 0; | |
padding: 0; | |
border: 0; | |
} | |
/* Base styles */ | |
body { | |
font-family: Arial, sans-serif !important; | |
margin: 0; | |
padding: 0; | |
color: #333; | |
} | |
/* Security alert banner */ | |
.security-alert { | |
background-color: #FFF4B5; | |
border-bottom: 1px solid #e6c949; | |
color: #222222; | |
font-family: Arial, sans-serif; | |
font-size: 12px; | |
font-weight: normal; | |
width: 100%; | |
margin: 0 0 22px 0; | |
padding: 8px 0; | |
text-align: center; | |
} | |
.security-alert img { | |
max-height: 16px; | |
vertical-align: middle; | |
margin-right: 5px; | |
} | |
/* Layout styles */ | |
.page-wrapper { | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
.logo-wrapper { | |
text-align: right; | |
margin-bottom: 15px; | |
} | |
.logo { | |
max-height: 25px; | |
} | |
.container { | |
background-color: #f9f9f9; | |
border: 1px solid #dddddd; | |
border-radius: 5px; | |
padding: 20px; | |
} | |
.header { | |
border-bottom: 1px solid #eeeeee; | |
padding-bottom: 10px; | |
margin-bottom: 20px; | |
} | |
h2 { | |
margin-top: 0; | |
margin-bottom: 15px; | |
} | |
.content { | |
margin-bottom: 20px; | |
} | |
/* User info styles */ | |
.user-info { | |
text-align: center; | |
margin: 15px auto; | |
padding: 10px; | |
background-color: #f0f0f0; | |
border-radius: 4px; | |
max-width: 80%; | |
} | |
.user-info p { | |
text-align: center; | |
margin: 5px 0; | |
} | |
/* Code display styles */ | |
.code { | |
background-color: #e0e0e0; | |
padding: 15px; | |
font-family: monospace; | |
font-size: 18px; | |
font-weight: bold; | |
text-align: center; | |
letter-spacing: 2px; | |
border-radius: 5px; | |
margin: 15px 0; | |
border: 1px solid #ccc; | |
} | |
.expire-time { | |
color: #cc0000; | |
font-weight: 500; | |
} | |
/* Footer styles */ | |
.footer { | |
padding-top: 10px; | |
margin-top: 20px; | |
color: #777777; | |
} | |
.security-note { | |
font-size: 11px; | |
color: #666; | |
margin-top: 20px; | |
border-top: 1px dotted #ddd; | |
padding-top: 10px; | |
} | |
/* Legal footer styles */ | |
.legal-wrapper { | |
text-align: center; | |
margin-top: 15px; | |
margin-bottom: 15px; | |
font-size: 10px; | |
color: #666666; | |
width: 100%; | |
} | |
.legal-logo { | |
max-height: 15px; | |
margin: 0 auto; | |
display: block; | |
} | |
.legal-wrapper p { | |
margin: 5px 0; | |
text-align: center; | |
} | |
.legal-wrapper a { | |
color: #666666; | |
text-decoration: none; | |
} | |
/* Typography styles */ | |
p { | |
margin: 10px 0; | |
} | |
a { | |
color: #0066cc; | |
text-decoration: underline; | |
font-weight: 500; | |
} | |
a:hover { | |
text-decoration: none; | |
} | |
ol { | |
padding-left: 25px; | |
} | |
li { | |
margin-bottom: 8px; | |
} | |
/* Mobile styles */ | |
@media screen and (max-width: 480px) { | |
.container { | |
padding: 15px 10px; | |
} | |
.code { | |
font-size: 14px; | |
word-break: break-all; | |
} | |
} | |
</style> | |
</head> | |
<body style="margin:0; padding:0; background-color:#ffffff; font-family:Arial, sans-serif;"> | |
<!-- Outer wrapper to prevent zoom-to-fit --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%; min-width:600px;"> | |
<tr> | |
<td align="center"> | |
<!-- Security alert banner --> | |
{{ emailBodyAlertBanner }} | |
<!-- Main content wrapper --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="min-width:600px;"> | |
<tr> | |
<td style="padding:20px;"> | |
<!-- Content container --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" align="center" style="max-width:600px;"> | |
<tr> | |
<!-- Logo row --> | |
<td align="right" style="padding-bottom:15px;"> | |
<img src="{{ image:tenantBannerLogo }}" alt="Company Logo" style="max-height:25px;" /> | |
</td> | |
</tr> | |
<tr> | |
<!-- Main container --> | |
<td style="background-color:#f9f9f9; border:1px solid #dddddd; border-radius:5px; padding:20px;"> | |
<!-- Header section --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="border-bottom:1px solid #eeeeee; padding-bottom:10px; margin-bottom:20px;"> | |
<h2 style="margin-top:0; margin-bottom:15px; font-family:Arial, sans-serif;">{{ emailTitle }}</h2> | |
</td> | |
</tr> | |
</table> | |
<!-- Content section --> | |
{{ emailBodyText }} | |
<!-- Footer section --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:20px; margin-top:20px; border-top:1px solid #eeeeee; color:#777777;"> | |
<p style="margin:10px 0;">Thank you for your assistance.</p> | |
<p style="margin:10px 0;">{{ emailClosing }}</p> | |
<p style="margin:20px 0; font-style:italic;">Note: If you have not made the request, you can safely ignore this e-mail.</p> | |
{{ emailBodyFooterHint }} | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Legal footer outside container --> | |
<tr> | |
<td align="center" style="padding-top:15px; font-size:10px; color:#666666;"> | |
<img src="{{ image:tenantBannerLogo }}" alt="Company Logo" style="max-height:15px; margin:0 auto; display:block;" /> | |
<p style="margin:5px 0; text-align:center;">{{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }}</p> | |
<p style="margin:5px 0; text-align:center;"> | |
<a href="{{ orgPrivacyStatementUrl }}" style="color:#666666; text-decoration:none;">Privacy Statement</a> | | |
<a href="mailto:{{ orgPrivacyContact }}" style="color:#666666; text-decoration:none;">Privacy Contact</a> | |
</p> | |
<p style="margin:5px 0; text-align:center;">Tenant ID: {{ orgTenantId }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
</table> | |
</body> | |
</html> | |
'@ | |
#endregion | |
#region DE template | |
de = @' | |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta name="x-apple-disable-message-reformatting" /> | |
<!--[if mso]> | |
<xml> | |
<o:OfficeDocumentSettings> | |
<o:AllowPNG/> | |
<o:PixelsPerInch>96</o:PixelsPerInch> | |
</o:OfficeDocumentSettings> | |
</xml> | |
<![endif]--> | |
<style type="text/css"> | |
/* Client-specific styles */ | |
#outlook a{padding:0;} | |
.ReadMsgBody{width:100%;} | |
.ExternalClass{width:100%;} | |
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div{line-height: 100%;} | |
<!--[if mso]> | |
body, table, td, div, p { | |
font-family: Arial, sans-serif !important; | |
} | |
table { | |
border-collapse: collapse !important; | |
mso-table-lspace: 0pt !important; | |
mso-table-rspace: 0pt !important; | |
} | |
<![endif]--> | |
/* Reset styles */ | |
html, body { | |
margin: 0; | |
padding: 0; | |
border: 0; | |
} | |
/* Base styles */ | |
body { | |
font-family: Arial, sans-serif !important; | |
margin: 0; | |
padding: 0; | |
color: #333; | |
} | |
/* Security alert banner */ | |
.security-alert { | |
background-color: #FFF4B5; | |
border-bottom: 1px solid #e6c949; | |
color: #222222; | |
font-family: Arial, sans-serif; | |
font-size: 12px; | |
font-weight: normal; | |
width: 100%; | |
margin: 0 0 22px 0; | |
padding: 8px 0; | |
text-align: center; | |
} | |
.security-alert img { | |
max-height: 16px; | |
vertical-align: middle; | |
margin-right: 5px; | |
} | |
/* Layout styles */ | |
.page-wrapper { | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
.logo-wrapper { | |
text-align: right; | |
margin-bottom: 15px; | |
} | |
.logo { | |
max-height: 25px; | |
} | |
.container { | |
background-color: #f9f9f9; | |
border: 1px solid #dddddd; | |
border-radius: 5px; | |
padding: 20px; | |
} | |
.header { | |
border-bottom: 1px solid #eeeeee; | |
padding-bottom: 10px; | |
margin-bottom: 20px; | |
} | |
h2 { | |
margin-top: 0; | |
margin-bottom: 15px; | |
} | |
.content { | |
margin-bottom: 20px; | |
} | |
/* User info styles */ | |
.user-info { | |
text-align: center; | |
margin: 15px auto; | |
padding: 10px; | |
background-color: #f0f0f0; | |
border-radius: 4px; | |
max-width: 80%; | |
} | |
.user-info p { | |
text-align: center; | |
margin: 5px 0; | |
} | |
/* Code display styles */ | |
.code { | |
background-color: #e0e0e0; | |
padding: 15px; | |
font-family: monospace; | |
font-size: 18px; | |
font-weight: bold; | |
text-align: center; | |
letter-spacing: 2px; | |
border-radius: 5px; | |
margin: 15px 0; | |
border: 1px solid #ccc; | |
} | |
.expire-time { | |
color: #cc0000; | |
font-weight: 500; | |
} | |
/* Footer styles */ | |
.footer { | |
padding-top: 10px; | |
margin-top: 20px; | |
color: #777777; | |
} | |
.security-note { | |
font-size: 11px; | |
color: #666; | |
margin-top: 20px; | |
border-top: 1px dotted #ddd; | |
padding-top: 10px; | |
} | |
/* Legal footer styles */ | |
.legal-wrapper { | |
text-align: center; | |
margin-top: 15px; | |
margin-bottom: 15px; | |
font-size: 10px; | |
color: #666666; | |
width: 100%; | |
} | |
.legal-logo { | |
max-height: 15px; | |
margin: 0 auto; | |
display: block; | |
} | |
.legal-wrapper p { | |
margin: 5px 0; | |
text-align: center; | |
} | |
.legal-wrapper a { | |
color: #666666; | |
text-decoration: none; | |
} | |
/* Typography styles */ | |
p { | |
margin: 10px 0; | |
} | |
a { | |
color: #0066cc; | |
text-decoration: underline; | |
font-weight: 500; | |
} | |
a:hover { | |
text-decoration: none; | |
} | |
ol { | |
padding-left: 25px; | |
} | |
li { | |
margin-bottom: 8px; | |
} | |
/* Mobile styles */ | |
@media screen and (max-width: 480px) { | |
.container { | |
padding: 15px 10px; | |
} | |
.code { | |
font-size: 14px; | |
word-break: break-all; | |
} | |
} | |
</style> | |
</head> | |
<body style="margin:0; padding:0; background-color:#ffffff; font-family:Arial, sans-serif;"> | |
<!-- Outer wrapper to prevent zoom-to-fit --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%; min-width:600px;"> | |
<tr> | |
<td align="center"> | |
<!-- Security alert banner --> | |
{{ emailBodyAlertBanner }} | |
<!-- Main content wrapper --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="min-width:600px;"> | |
<tr> | |
<td style="padding:20px;"> | |
<!-- Content container --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" align="center" style="max-width:600px;"> | |
<tr> | |
<!-- Logo row --> | |
<td align="right" style="padding-bottom:15px;"> | |
<img src="{{ image:tenantBannerLogo }}" alt="Company Logo" style="max-height:25px;" /> | |
</td> | |
</tr> | |
<tr> | |
<!-- Main container --> | |
<td style="background-color:#f9f9f9; border:1px solid #dddddd; border-radius:5px; padding:20px;"> | |
<!-- Header section --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="border-bottom:1px solid #eeeeee; padding-bottom:10px; margin-bottom:20px;"> | |
<h2 style="margin-top:0; margin-bottom:15px; font-family:Arial, sans-serif;">{{ emailTitle }}</h2> | |
</td> | |
</tr> | |
</table> | |
<!-- Content section --> | |
{{ emailBodyText }} | |
<!-- Footer section --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:20px; margin-top:20px; border-top:1px solid #eeeeee; color:#777777;"> | |
<p style="margin:10px 0;">Vielen Dank für Ihre Unterstützung.</p> | |
<p style="margin:10px 0;">{{ emailClosing }}</p> | |
<p style="margin:20px 0; font-style:italic;">Hinweis: Wenn Sie die Anfrage nicht gestellt haben, können Sie diese E-Mail ohne Bedenken ignorieren.</p> | |
{{ emailBodyFooterHint }} | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Legal footer outside container --> | |
<tr> | |
<td align="center" style="padding-top:15px; font-size:10px; color:#666666;"> | |
<img src="{{ image:tenantBannerLogo }}" alt="Company Logo" style="max-height:15px; margin:0 auto; display:block;" /> | |
<p style="margin:5px 0; text-align:center;">{{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }}</p> | |
<p style="margin:5px 0; text-align:center;"> | |
<a href="{{ orgPrivacyStatementUrl }}" style="color:#666666; text-decoration:none;">Datenschutzerklärung</a> | | |
<a href="mailto:{{ orgPrivacyContact }}" style="color:#666666; text-decoration:none;">Datenschutzkontakt</a> | |
</p> | |
<p style="margin:5px 0; text-align:center;">Mandanten-ID: {{ orgTenantId }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
</table> | |
</body> | |
</html> | |
'@ | |
#endregion | |
#region FR template | |
fr = @' | |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta name="x-apple-disable-message-reformatting" /> | |
<!--[if mso]> | |
<xml> | |
<o:OfficeDocumentSettings> | |
<o:AllowPNG/> | |
<o:PixelsPerInch>96</o:PixelsPerInch> | |
</o:OfficeDocumentSettings> | |
</xml> | |
<![endif]--> | |
<style type="text/css"> | |
/* Client-specific styles */ | |
#outlook a{padding:0;} | |
.ReadMsgBody{width:100%;} | |
.ExternalClass{width:100%;} | |
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div{line-height: 100%;} | |
<!--[if mso]> | |
body, table, td, div, p { | |
font-family: Arial, sans-serif !important; | |
} | |
table { | |
border-collapse: collapse !important; | |
mso-table-lspace: 0pt !important; | |
mso-table-rspace: 0pt !important; | |
} | |
<![endif]--> | |
/* Reset styles */ | |
html, body { | |
margin: 0; | |
padding: 0; | |
border: 0; | |
} | |
/* Base styles */ | |
body { | |
font-family: Arial, sans-serif !important; | |
margin: 0; | |
padding: 0; | |
color: #333; | |
} | |
/* Security alert banner */ | |
.security-alert { | |
background-color: #FFF4B5; | |
border-bottom: 1px solid #e6c949; | |
color: #222222; | |
font-family: Arial, sans-serif; | |
font-size: 12px; | |
font-weight: normal; | |
width: 100%; | |
margin: 0 0 22px 0; | |
padding: 8px 0; | |
text-align: center; | |
} | |
.security-alert img { | |
max-height: 16px; | |
vertical-align: middle; | |
margin-right: 5px; | |
} | |
/* Layout styles */ | |
.page-wrapper { | |
max-width: 600px; | |
margin: 0 auto; | |
} | |
.logo-wrapper { | |
text-align: right; | |
margin-bottom: 15px; | |
} | |
.logo { | |
max-height: 25px; | |
} | |
.container { | |
background-color: #f9f9f9; | |
border: 1px solid #dddddd; | |
border-radius: 5px; | |
padding: 20px; | |
} | |
.header { | |
border-bottom: 1px solid #eeeeee; | |
padding-bottom: 10px; | |
margin-bottom: 20px; | |
} | |
h2 { | |
margin-top: 0; | |
margin-bottom: 15px; | |
} | |
.content { | |
margin-bottom: 20px; | |
} | |
/* User info styles */ | |
.user-info { | |
text-align: center; | |
margin: 15px auto; | |
padding: 10px; | |
background-color: #f0f0f0; | |
border-radius: 4px; | |
max-width: 80%; | |
} | |
.user-info p { | |
text-align: center; | |
margin: 5px 0; | |
} | |
/* Code display styles */ | |
.code { | |
background-color: #e0e0e0; | |
padding: 15px; | |
font-family: monospace; | |
font-size: 18px; | |
font-weight: bold; | |
text-align: center; | |
letter-spacing: 2px; | |
border-radius: 5px; | |
margin: 15px 0; | |
border: 1px solid #ccc; | |
} | |
.expire-time { | |
color: #cc0000; | |
font-weight: 500; | |
} | |
/* Footer styles */ | |
.footer { | |
padding-top: 10px; | |
margin-top: 20px; | |
color: #777777; | |
} | |
.security-note { | |
font-size: 11px; | |
color: #666; | |
margin-top: 20px; | |
border-top: 1px dotted #ddd; | |
padding-top: 10px; | |
} | |
/* Legal footer styles */ | |
.legal-wrapper { | |
text-align: center; | |
margin-top: 15px; | |
margin-bottom: 15px; | |
font-size: 10px; | |
color: #666666; | |
width: 100%; | |
} | |
.legal-logo { | |
max-height: 15px; | |
margin: 0 auto; | |
display: block; | |
} | |
.legal-wrapper p { | |
margin: 5px 0; | |
text-align: center; | |
} | |
.legal-wrapper a { | |
color: #666666; | |
text-decoration: none; | |
} | |
/* Typography styles */ | |
p { | |
margin: 10px 0; | |
} | |
a { | |
color: #0066cc; | |
text-decoration: underline; | |
font-weight: 500; | |
} | |
a:hover { | |
text-decoration: none; | |
} | |
ol { | |
padding-left: 25px; | |
} | |
li { | |
margin-bottom: 8px; | |
} | |
/* Mobile styles */ | |
@media screen and (max-width: 480px) { | |
.container { | |
padding: 15px 10px; | |
} | |
.code { | |
font-size: 14px; | |
word-break: break-all; | |
} | |
} | |
</style> | |
</head> | |
<body style="margin:0; padding:0; background-color:#ffffff; font-family:Arial, sans-serif;"> | |
<!-- Outer wrapper to prevent zoom-to-fit --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="width:100%; min-width:600px;"> | |
<tr> | |
<td align="center"> | |
<!-- Security alert banner --> | |
{{ emailBodyAlertBanner }} | |
<!-- Main content wrapper --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" style="min-width:600px;"> | |
<tr> | |
<td style="padding:20px;"> | |
<!-- Content container --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0" align="center" style="max-width:600px;"> | |
<tr> | |
<!-- Logo row --> | |
<td align="right" style="padding-bottom:15px;"> | |
<img src="{{ image:tenantBannerLogo }}" alt="Company Logo" style="max-height:25px;" /> | |
</td> | |
</tr> | |
<tr> | |
<!-- Main container --> | |
<td style="background-color:#f9f9f9; border:1px solid #dddddd; border-radius:5px; padding:20px;"> | |
<!-- Header section --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="border-bottom:1px solid #eeeeee; padding-bottom:10px; margin-bottom:20px;"> | |
<h2 style="margin-top:0; margin-bottom:15px; font-family:Arial, sans-serif;">{{ emailTitle }}</h2> | |
</td> | |
</tr> | |
</table> | |
<!-- Content section --> | |
{{ emailBodyText }} | |
<!-- Footer section --> | |
<table role="presentation" width="100%" border="0" cellspacing="0" cellpadding="0"> | |
<tr> | |
<td style="padding-top:20px; margin-top:20px; border-top:1px solid #eeeeee; color:#777777;"> | |
<p style="margin:10px 0;">Merci pour votre collaboration.</p> | |
<p style="margin:10px 0;">{{ emailClosing }}</p> | |
<p style="margin:20px 0; font-style:italic;">Remarque : si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité.</p> | |
{{ emailBodyFooterHint }} | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
<!-- Legal footer outside container --> | |
<tr> | |
<td align="center" style="padding-top:15px; font-size:10px; color:#666666;"> | |
<img src="{{ image:tenantBannerLogo }}" alt="Company Logo" style="max-height:15px; margin:0 auto; display:block;" /> | |
<p style="margin:5px 0; text-align:center;">{{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }}</p> | |
<p style="margin:5px 0; text-align:center;"> | |
<a href="{{ orgPrivacyStatementUrl }}" style="color:#666666; text-decoration:none;">Déclaration de confidentialité</a> | | |
<a href="mailto:{{ orgPrivacyContact }}" style="color:#666666; text-decoration:none;">Contact confidentialité</a> | |
</p> | |
<p style="margin:5px 0; text-align:center;">ID du client (Tenant ID) : {{ orgTenantId }}</p> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
</table> | |
</td> | |
</tr> | |
</table> | |
</body> | |
</html> | |
'@ | |
#endregion | |
} | |
#endregion | |
if ([string]::IsNullOrEmpty($EmailTemplate)) { | |
if ($EmailTemplateTranslations.ContainsKey($languageCode)) { | |
Write-Verbose "Using custom HTML email template for $languageCode" | |
$bodyContentTemplate = $EmailTemplateTranslations[$languageCode] | |
} | |
else { | |
Write-Verbose 'Using default English HTML email template' | |
$bodyContentTemplate = $EmailTemplateTranslations['en'] | |
} | |
} | |
else { | |
try { | |
$EmailTemplates = ConvertFrom-Json -InputObject $EmailTemplate -AsHashtable -ErrorAction Stop | |
if ($EmailTemplates -is [hashtable]) { | |
if ($EmailTemplates.ContainsKey($languageCode)) { | |
Write-Verbose "Using custom language-specific HTML email template for $languageCode" | |
$bodyContentTemplate = $EmailTemplates[$languageCode] | |
} | |
elseif ($EmailClosings.ContainsKey('en')) { | |
Write-Verbose 'Using custom default English HTML email template' | |
$bodyContentTemplate = $EmailTemplates['en'] | |
} | |
else { | |
Write-Verbose 'No default English template found, using default HTML template' | |
$bodyContentTemplate = $EmailTemplateTranslations['en'] | |
} | |
} | |
} | |
catch { | |
Write-Verbose 'EmailTemplate: Not a valid JSON object found, interpreting as raw HTML' | |
$bodyContentTemplate = $EmailTemplate | |
} | |
} | |
} | |
#endregion | |
#region Text Email | |
else { | |
$bodyContentType = 'Text' | |
$EmailBodyFooterHintTranslations = @{ | |
en = "This is an automated email containing a temporary access pass. For security reasons, the code will expire after use or after the time indicated above.`n`n" | |
de = "Diese E-Mail wurde automatisch erstellt und enthält einen vorübergehenden Zugriffscode. Aus Sicherheitsgründen läuft der Code nach der Verwendung oder nach der oben angegebenen Zeit ab.`n`n" | |
fr = "Ceci est un e-mail automatique contenant un code d’accès temporaire. Pour des raisons de sécurité, le code expirera après son utilisation ou après l’heure d’expiration indiquée ci-dessus.`n`n" | |
} | |
if ($Reason -ne 'TAPCreated') { | |
$templateVariables.emailBodyFooterHint = "`n" | |
} | |
else { | |
$templateVariables.emailBodyFooterHint = if ($EmailBodyFooterHintTranslations.ContainsKey($languageCode)) { | |
$EmailBodyFooterHintTranslations[$languageCode] | |
} | |
else { | |
$EmailBodyFooterHintTranslations['en'] | |
} | |
} | |
$EmailBodyTextTranslations = @{ | |
en = { | |
TAPCreated = @' | |
A Temporary Access Pass code for onboarding has been created for the following person: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Please provide the following code to {{ userGivenName }}: | |
{{ temporaryAccessPass }} | |
The code will expire in {{ lifeTimeInHoursMinutes }}, at {{ expirationTime }} UTC. | |
Next steps for {{ userGivenName }}: | |
1. Use the code to sign in when prompted. | |
2. Set up the Microsoft Authenticator app or another authentication method. | |
3. After setup is complete, the Temporary Access Pass can be deleted at https://aka.ms/MySecurityInfo . | |
4. From now on, use the new authentication methods you have set up. | |
'@ | |
UserBlocked = @' | |
A Temporary Access Pass code could not be created for the following person: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
The creation was blocked because this type of user account is not allowed to use this process to create a Temporary Access Pass code. | |
Next steps if {{ userGivenName }} is not able to sign in: | |
1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. | |
2. The support team will validate the request and provide guidance on how to proceed. | |
'@ | |
TAPNotEnabled = @' | |
A Temporary Access Pass code could not be created for the following person: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
The creation was aborted because the Temporary Access Pass feature is not enabled for this user account. | |
Next steps if {{ userGivenName }} is not able to sign in: | |
1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. | |
2. The support team will validate the request and provide guidance on how to proceed. | |
'@ | |
TAPExpiredWithOtherMethodsConfigured = @' | |
A Temporary Access Pass code could not be created for the following person: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured. | |
The previously issued Temporary Access Pass is no longer valid. | |
Please ask {{ userGivenName }} to sign in using their existing authentication methods. | |
Next steps if {{ userGivenName }} is not able to sign in: | |
1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. | |
2. The support team will validate the request and provide guidance on how to proceed. | |
'@ | |
TAPActiveWithOtherMethodsConfigured = @' | |
A Temporary Access Pass code could not be created for the following person: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
The creation was aborted because {{ userGivenName }} has already completed the onboarding process and has other authentication methods configured. | |
{{ userGivenName }} must use the existing Temporary Access Pass to sign in before being able to use the other authentication methods. | |
If {{ userGivenName }} is able to sign in using the active Temporary Access Pass, they can remove it themselves at https://aka.ms/MySecurityInfo to start using their other authentication methods. | |
Next steps if {{ userGivenName }} is not able to sign in: | |
1. Ask {{ userGivenName }} to contact IT support and request removal of the active Temporary Access Pass, or wait until the code expires. | |
2. {{ userGivenName }} may then try to sign in using one of their existing authentication methods. | |
3. The support team will provide further guidance to restore access to the account if needed. | |
'@ | |
OtherMethodsConfigured = @' | |
A Temporary Access Pass code could not be created for the following person: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
The creation was aborted because {{ userGivenName }} has already completed the onboarding process and already has authentication methods configured. | |
Temporary Access Pass codes must no longer be issued via the self-service process and require handling by IT support. | |
Please ask {{ userGivenName }} to sign in using their existing authentication methods. | |
Next steps if {{ userGivenName }} is not able to sign in: | |
1. Ask {{ userGivenName }} to contact IT support and request a reset of their authentication methods. | |
2. The support team will validate the request and provide guidance on how to proceed. | |
'@ | |
} | |
de = { | |
TAPCreated = @' | |
Ein befristeter Zugriffscode für den Onboarding-Prozess wurde für die folgende Person erstellt: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Bitte geben Sie den folgenden Code an {{ userGivenName }} weiter: | |
{{ temporaryAccessPass }} | |
Der Code läuft in {{ lifeTimeInHoursMinutes }} um {{ expirationTime }} (GMT) ab. | |
Nächste Schritte für {{ userGivenName }}: | |
1. Verwenden Sie den Code, um sich anzumelden, wenn Sie dazu aufgefordert werden. | |
2. Richten Sie die Microsoft Authenticator-App oder eine andere Authentifizierungsmethode ein. | |
3. Nach Abschluss der Einrichtung kann der befristete Zugriffscode unter https://aka.ms/MySecurityInfo gelöscht werden. | |
4. Nutzen Sie von nun an die neuen Authentifizierungsmethoden, die Sie eingerichtet haben. | |
'@ | |
UserBlocked = @' | |
Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Die Erstellung wurde blockiert, da dieser Kontotyp nicht berechtigt ist, über diesen Prozess einen befristeten Zugriffscode zu erhalten. | |
Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: | |
1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. | |
2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. | |
'@ | |
TAPNotEnabled = @' | |
Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Die Erstellung wurde abgebrochen, da die Funktion für befristete Zugriffscodes für dieses Konto nicht aktiviert ist. | |
Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: | |
1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. | |
2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. | |
'@ | |
TAPExpiredWithOtherMethodsConfigured = @' | |
Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind. | |
Der zuvor ausgestellte Zugriffscode ist nicht mehr gültig. | |
Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden. | |
Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: | |
1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. | |
2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. | |
'@ | |
TAPActiveWithOtherMethodsConfigured = @' | |
Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind. | |
{{ userGivenName }} muss den bestehenden befristeten Zugriffscode verwenden, um sich anzumelden, bevor andere Authentifizierungsmethoden genutzt werden können. | |
Kann sich {{ userGivenName }} erfolgreich mit dem aktiven Zugriffscode anmelden, kann dieser unter https://aka.ms/MySecurityInfo eigenständig entfernt werden, um anschließend die anderen Authentifizierungsmethoden zu verwenden. | |
Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: | |
1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und die Entfernung des aktiven Zugriffscodes anzufordern, oder warten Sie die Ablaufzeit ab. | |
2. Anschließend kann {{ userGivenName }} versuchen, sich mit einer bestehenden Authentifizierungsmethode anzumelden. | |
3. Falls nötig, unterstützt der Support bei der Wiederherstellung des Kontozugriffs. | |
'@ | |
OtherMethodsConfigured = @' | |
Ein befristeter Zugriffscode konnte für die folgende Person nicht erstellt werden: | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Die Erstellung wurde abgebrochen, da der Onboarding-Prozess bereits abgeschlossen wurde und andere Authentifizierungsmethoden für dieses Konto eingerichtet sind. | |
Befristete Zugriffscodes dürfen nach Abschluss des Onboardings nicht mehr über den Self-Service-Prozess erstellt werden und erfordern die Unterstützung durch den IT-Support. | |
Bitten Sie {{ userGivenName }}, sich mit den bestehenden Authentifizierungsmethoden anzumelden. | |
Nächste Schritte, falls {{ userGivenName }} sich nicht anmelden kann: | |
1. Bitten Sie {{ userGivenName }}, den IT-Support zu kontaktieren und eine Zurücksetzung der Authentifizierungsmethoden anzufordern. | |
2. Der Support prüft die Anfrage und hilft bei den nächsten Schritten. | |
'@ | |
} | |
fr = { | |
TAPCreated = @' | |
Un code d’accès temporaire pour l’intégration a été créé pour la personne suivant : | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
Veuillez transmettre le code suivant à {{ userGivenName }} : | |
{{ temporaryAccessPass }} | |
Le code expirera dans {{ lifeTimeInHoursMinutes }}, à {{ expirationTime }} UTC. | |
Prochaines étapes pour {{ userGivenName }} : | |
1. Utilisez le code pour vous connecter lorsque cela vous est demandé. | |
2. Configurez l’application Microsoft Authenticator ou une autre méthode d’authentification. | |
3. Une fois la configuration terminée, le code d’accès temporaire peut être supprimé via https://aka.ms/MySecurityInfo. | |
4. Utilisez ensuite les nouvelles méthodes d’authentification que vous avez configurées. | |
'@ | |
UserBlocked = @' | |
Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
La création a été bloquée car ce type de compte n’est pas autorisé à utiliser ce processus pour obtenir un code d’accès temporaire. | |
Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : | |
1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification. | |
2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. | |
'@ | |
TAPNotEnabled = @' | |
Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
La création a été annulée car la fonctionnalité de code d’accès temporaire n’est pas activée pour ce compte. | |
Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : | |
1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d'authentification. | |
2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. | |
'@ | |
TAPExpiredWithOtherMethodsConfigured = @' | |
Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées. | |
Le code d’accès temporaire précédemment émis n’est plus valide. | |
Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes. | |
Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : | |
1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification. | |
2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. | |
'@ | |
TAPActiveWithOtherMethodsConfigured = @' | |
Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification ont été configurées. | |
{{ userGivenName }} doit utiliser le code d’accès temporaire existant pour se connecter avant de pouvoir utiliser d’autres méthodes d’authentification. | |
Si {{ userGivenName }} parvient à se connecter avec le code d’accès temporaire actif, il peut le supprimer lui-même sur https://aka.ms/MySecurityInfo afin d’utiliser ses autres méthodes d’authentification. | |
Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : | |
1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la suppression du code d’accès temporaire actif, ou d’attendre son expiration. | |
2. Ensuite, {{ userGivenName }} peut essayer de se connecter avec une autre méthode d’authentification existante. | |
3. Si nécessaire, l’assistance informatique aidera à restaurer l’accès au compte. | |
'@ | |
OtherMethodsConfigured = @' | |
Un code d’accès temporaire n’a pas pu être créé pour la personne suivante : | |
{{ userDisplayName }} | |
{{ userPrincipalName }} | |
La création a été annulée car l’intégration de l’utilisateur est terminée et d’autres méthodes d’authentification sont déjà configurées. | |
Après l’intégration, les codes d’accès temporaires ne doivent plus être délivrés via le processus en libre-service et nécessitent l’intervention de l’assistance informatique. | |
Veuillez demander à {{ userGivenName }} de se connecter en utilisant ses méthodes d’authentification existantes. | |
Prochaines étapes si {{ userGivenName }} ne parvient pas à se connecter : | |
1. Demandez à {{ userGivenName }} de contacter l’assistance informatique et de demander la réinitialisation de ses méthodes d’authentification. | |
2. L’équipe d’assistance validera la demande et fournira des instructions pour la suite. | |
'@ | |
} | |
} | |
#region Text email template | |
$EmailTemplateTranslations = @{ | |
#region EN template | |
en = @' | |
{{ emailSalutation }} | |
{{ bodyText }} | |
Thank you for your assistance. | |
{{ emailClosing }} | |
Note: If you have not made the request, you can safely ignore this e-mail. | |
-- | |
{{ emailBodyFooterHint }}{{ orgDisplayName }}, {{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }} | |
Tenant ID: {{ orgTenantId }} | |
'@ | |
#endregion | |
#region DE template | |
de = @' | |
{{ emailSalutation }} | |
{{ bodyText }} | |
Vielen Dank für Ihre Unterstützung. | |
{{ emailClosing }} | |
Hinweis: Wenn Sie die Anfrage nicht gestellt haben, können Sie diese E-Mail ohne Bedenken ignorieren. | |
-- | |
{{ emailBodyFooterHint }}{{ orgDisplayName }}, {{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }} | |
Mandanten-ID: {{ orgTenantId }} | |
'@ | |
#endregion | |
#region FR template | |
fr = @' | |
{{ emailSalutation }} | |
{{ bodyText }} | |
Merci pour votre collaboration. | |
{{ emailClosing }} | |
Remarque : si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer cet e-mail en toute sécurité. | |
-- | |
{{ emailBodyFooterHint }}{{ orgDisplayName }}, {{ orgStreet }}, {{ orgPostalCode }} {{ orgCity }}, {{ orgCountry }} | |
ID du client (Tenant ID) : {{ orgTenantId }} | |
'@ | |
#endregion | |
} | |
#endregion | |
if ([string]::IsNullOrEmpty($EmailTemplate)) { | |
if ($EmailTemplateTranslations.ContainsKey($languageCode)) { | |
Write-Verbose "Using custom text email template for $languageCode" | |
$bodyContentTemplate = $EmailTemplateTranslations[$languageCode] | |
} | |
else { | |
Write-Verbose 'Using default English text email template' | |
$bodyContentTemplate = $EmailTemplateTranslations['en'] | |
} | |
} | |
else { | |
try { | |
$EmailTemplates = ConvertFrom-Json -InputObject $EmailTemplate -AsHashtable -ErrorAction Stop | |
if ($EmailTemplates -is [hashtable]) { | |
if ($EmailTemplates.ContainsKey($languageCode)) { | |
$bodyContentTemplate = $EmailTemplates[$languageCode] | |
} | |
elseif ($EmailClosings.ContainsKey('en')) { | |
$bodyContentTemplate = $EmailTemplates['en'] | |
} | |
else { | |
throw 'No default English template found' | |
} | |
} | |
} | |
catch { | |
Write-Verbose 'EmailTemplate: Not a valid JSON object found, interpreting as raw text' | |
$bodyContentTemplate = $EmailTemplate | |
} | |
} | |
} | |
#endregion | |
# Expand template with variables | |
$bodyContent = Expand-Template -Template $bodyContentTemplate -Variables $templateVariables | |
# Extract all image references using regex to find actually used images | |
if ($EmailImages.Count -gt 0 -and $UseHtmlEmail) { | |
$usedImageKeys = @() | |
# Find all occurrences of "cid:imageName" in the expanded template | |
$cidMatches = [regex]::Matches($bodyContent, 'cid:([a-zA-Z0-9_\-]+)') | |
foreach ($match in $cidMatches) { | |
if ($match.Groups.Count -gt 1) { | |
$usedImageKeys += $match.Groups[1].Value | |
} | |
} | |
Write-Verbose "Found $($usedImageKeys.Count) image references in template" | |
# Only process images that are actually used in the template | |
$emailAttachments = @() | |
foreach ($imageKey in $EmailImages.Keys) { | |
if ($imageKey -in $usedImageKeys) { | |
$imageBase64 = $EmailImages[$imageKey] | |
# Validate the base64 data | |
try { | |
# Handle both clean base64 and data URLs | |
if ($imageBase64 -match '^data:(.*?);base64,(.+)$') { | |
$contentType = $Matches[1] | |
$cleanBase64 = $Matches[2] | |
} | |
else { | |
# Assume it's plain base64 | |
$contentType = 'image/png' # Default content type | |
$cleanBase64 = $imageBase64 | |
} | |
# Validate base64 string | |
$null = [Convert]::FromBase64String($cleanBase64) | |
$emailAttachments += @{ | |
'@odata.type' = '#microsoft.graph.fileAttachment' | |
name = "$imageKey" | |
contentType = $contentType | |
contentBytes = $cleanBase64 | |
contentId = $imageKey | |
isInline = $true | |
} | |
Write-Verbose "Added image attachment: $imageKey ($contentType)" | |
} | |
catch { | |
Write-Warning "Invalid base64 image data for key '$imageKey': $_" | |
} | |
} | |
} | |
} | |
# Construct email payload for Graph API | |
$emailContent = @{ | |
message = @{ | |
subject = $templateVariables.emailSubject | |
body = @{ | |
contentType = $bodyContentType | |
content = $bodyContent | |
} | |
toRecipients = @( | |
@{ | |
emailAddress = @{ | |
address = $templateVariables.managerMail | |
} | |
} | |
) | |
internetMessageHeaders = @( | |
@{ | |
name = 'X-Classification' | |
value = if ($Reason -eq 'TAPCreated') { 'Confidential' } else { 'InternalOnly' } | |
} | |
@{ | |
name = 'X-Exchange-Restrict' | |
value = 'InternalOnly' | |
} | |
@{ | |
name = 'X-No-Archive' | |
value = 'Yes' | |
} | |
@{ | |
name = 'X-Protect-Delivery' | |
value = 'Secure' | |
} | |
@{ | |
name = 'X-Sensitivity' | |
value = if ($Reason -eq 'TAPCreated') { 'Company-Confidential' } else { 'Company-Internal' } | |
} | |
) | |
importance = if ($Reason -eq 'TAPCreated') { 'high' } else { 'normal' } | |
} | |
saveToSentItems = 'false' | |
} | |
# Add attachments only if we have any | |
if ($emailAttachments.Count -gt 0) { | |
$emailContent.message.attachments = $emailAttachments | |
Write-Verbose "Added $($emailAttachments.Count) attachments to email" | |
} | |
if ($PSCmdlet.ShouldProcess( | |
"Send Temporary Access Pass email to $($templateVariables.managerMail) from $SenderEmailAddress", | |
"Do you want to send the TAP code for $($return.Data.DisplayName) to their manager at $($templateVariables.managerMail)?", | |
'Send Email with Temporary Access Pass' | |
)) { | |
try { | |
$null = Invoke-ResilientRemoteCall { | |
if (-not $WhatIfPreference) { | |
Invoke-MgGraphRequest -Method POST ` | |
-Uri "/v1.0/users/$SenderEmailAddress/sendMail" ` | |
-Body ($emailContent | ConvertTo-Json -Depth 4) ` | |
-ErrorAction Stop | |
Write-Output "Temporary Access Pass code sent to manager $($templateVariables.managerMail) from $SenderEmailAddress" | |
} | |
else { | |
Write-Verbose "What If: Would send email from $SenderEmailAddress to $($templateVariables.managerMail) with subject 'Temporary Access Pass for $($return.Data.DisplayName)'" | |
} | |
} | |
} | |
catch { | |
# delete the TAP if email sending fails | |
try { | |
$null = Invoke-ResilientRemoteCall { | |
if (-not $WhatIfPreference) { | |
Invoke-MgGraphRequest -Method DELETE ` | |
-Uri "/v1.0/users/$($userObj.id)/authentication/temporaryAccessPassMethods/$($return.Data.TemporaryAccessPass.Id)" ` | |
-ErrorAction Stop | |
} | |
} | |
} | |
catch { | |
Write-Error "Failed to delete TAP after email sending failed: $_" | |
} | |
throw "Failed to send Temporary Access Pass code to manager $($templateVariables.managerMail): $_" | |
} | |
} | |
else { | |
Write-Verbose "Email sending was canceled by user" | |
} | |
} | |
#endregion | |
#region Azure Automation Webhook data | |
if ($WebhookData) { | |
# Convert $WebhookData from JSON string to object | |
if ($WebhookData -is [string]) { | |
try { | |
$WebhookData = ConvertFrom-Json -InputObject $WebhookData -ErrorAction Stop | |
Write-Verbose 'Converted WebhookData JSON to object' | |
} | |
catch { | |
throw "Invalid WebhookData JSON: $_" | |
} | |
} | |
Write-Verbose "WEBHOOK-START" | |
Write-Verbose ("object type: {0}" -f $WebhookData.gettype()) | |
Write-Verbose $WebhookData | |
Write-Verbose "`n`n" | |
Write-Verbose $WebhookData.WebhookName | |
Write-Verbose $WebhookData.RequestBody | |
Write-Verbose $WebhookData.RequestHeader | |
Write-Verbose "WEBHOOK-END" | |
if ([string]::IsNullOrEmpty($WebhookSignatureKey)) { | |
try { | |
$WebhookSignatureKey = Get-AutomationVariable -Name 'AuthConfig_WebhookSignatureKey' -ErrorAction Stop | ConvertTo-SecureString -AsPlainText -Force | |
} | |
catch { | |
throw 'WebhookSignatureKey is required when using a webhook' | |
} | |
} | |
if (-not (Test-HmacAuthorization -SharedSecret $WebhookSignatureKey -WebhookData $WebhookData)) { | |
[void] $WebhookSignatureKey.Dispose() | |
throw 'Unauthorized webhook request' | |
} | |
[void] $WebhookSignatureKey.Dispose() | |
Write-Verbose 'Webhook request authorized' | |
try { | |
$request = ConvertFrom-Json -InputObject $WebhookData.RequestBody -ErrorAction Stop | |
Write-Verbose 'Request JSON parsed' | |
} | |
catch { | |
throw 'Invalid Webhook RequestBody JSON' | |
} | |
# Always send email to manager when using a webhook | |
$SendEmailToManager = $true | |
try { | |
# Validate and assign parameters | |
if ($null -ne $request.UserId) { | |
if (Assert-ParameterType -Name 'UserId' -Value $request.UserId -ExpectedTypes ([string])) { | |
$UserId = $request.UserId | |
} | |
} | |
if ($null -ne $request.StartDateTime) { | |
if (Assert-ParameterType -Name 'StartDateTime' -Value $request.StartDateTime -ExpectedTypes ([datetime])) { | |
$StartDateTime = $request.StartDateTime | |
} | |
} | |
if ($null -ne $request.LifetimeInMinutes) { | |
if (Assert-ParameterType -Name 'LifetimeInMinutes' -Value $request.LifetimeInMinutes -ExpectedTypes ([Int32], [Int64])) { | |
try { | |
$LifetimeInMinutes = [Int32] $request.LifetimeInMinutes | |
} | |
catch { | |
throw "Parameter 'LifetimeInMinutes' must be a valid integer" | |
} | |
} | |
} | |
if ($null -ne $request.IsUsableOnce) { | |
if (Assert-ParameterType -Name 'IsUsableOnce' -Value $request.IsUsableOnce -ExpectedTypes ([bool])) { | |
$IsUsableOnce = $request.IsUsableOnce | |
} | |
} | |
if ($null -ne $request.EmailLanguage) { | |
if (Assert-ParameterType -Name 'EmailLanguage' -Value $request.EmailLanguage -ExpectedTypes ([string])) { | |
$EmailLanguage = $request.EmailLanguage | |
} | |
} | |
# Check for other unexpected parameters | |
$validParameters = @( | |
'UserId', 'StartDateTime', 'LifetimeInMinutes', 'IsUsableOnce', 'EmailLanguage' | |
) | |
$unexpectedParams = $request.PSObject.Properties.Name | Where-Object { $_ -notin $validParameters } | |
if ($unexpectedParams) { | |
Write-Warning "Ignoring unexpected parameters: $($unexpectedParams -join ', ')" | |
} | |
} | |
catch { | |
throw "Invalid Webhook RequestBody: $_" | |
} | |
} | |
#endregion | |
if (-not $UserId) { | |
throw 'UserId is required.' | |
} | |
if (-not $WhatIfPreference -and $Simulate) { | |
$WhatIfPreference = $true | |
} | |
if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { | |
$OutJson = $true | |
$ProgressPreference = 'SilentlyContinue' | |
} | |
# Initialize return object as ordered for consistent output | |
$return = [ordered]@{} | |
# Define required Microsoft Graph scopes | |
$mgScopes = [string[]]@( | |
'User.Read.All' # To read user information, inlcuding EmployeeHireDate | |
'UserAuthenticationMethod.ReadWrite.All' # To update authentication methods (TAP) of the user | |
'Policy.Read.All' # To read and validate current policy settings | |
'Directory.Read.All' # To read directory data and settings | |
) | |
# Make sure Mail.Send permission is included if SendEmailToManager is requested | |
if ($SendEmailToManager) { | |
$mgScopes += 'MailboxSettings.Read' # To read mailbox language | |
$mgScopes += 'Mail.Send' # To send email on behalf of a shared mailbox | |
} | |
# User validation - reject specific account types | |
$invalidUserRegexPatterns = @( | |
'^A[0-9][A-Z][-_].+@.+$', # Tiered admin accounts | |
'^ADM[CL]?[-_].+@.+$', # Non-Tiered admin accounts | |
'^.+#EXT#@.+\.onmicrosoft\.com$', # External Accounts | |
'^(?:SVCC?_.+|SVC[A-Z0-9]+)@.+$', # Service Accounts | |
'^(?:Sync_.+|[A-Z]+SyncServiceAccount.*)@.+$' # Entra Sync Accounts | |
) | |
if ($invalidUserRegexPatterns | Where-Object { $UserId -match $_ }) { | |
Write-Error 'This type of user can not have a Temporary Access Pass created using this process.' | |
exit 1 | |
} | |
# Handle Graph authentication | |
if (-not (Get-MgContext)) { | |
try { | |
if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { | |
$null = Invoke-ResilientRemoteCall { Connect-MgGraph -Identity -ContextScope Process } | |
Write-Verbose "Connected with managed identity: $((Get-MgContext).Account)" | |
} | |
else { | |
$null = Invoke-ResilientRemoteCall { Connect-MgGraph -Scopes $mgScopes -ContextScope Process } | |
} | |
} | |
catch { | |
throw "Failed to connect to Microsoft Graph: $_" | |
} | |
} | |
# Check if current scopes are sufficient | |
$missingScopes = [List[string]]::new() | |
$currentScopes = (Get-MgContext).Scopes | |
foreach ($scope in $mgScopes) { | |
# Skip write scopes if in WhatIf mode | |
if ($WhatIfPreference -and $scope -like '*Write*') { | |
Write-Verbose "What If: Removed $scope from required Microsoft Graph scopes" | |
continue | |
} | |
if ($scope -notin $currentScopes) { | |
$missingScopes.Add($scope) | |
} | |
} | |
if ($missingScopes.Count -gt 0) { | |
if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { | |
throw "Missing Microsoft Graph authorization scopes:`n$($missingScopes -join "`n")" | |
} | |
else { | |
# Reconnect with required scopes | |
try { | |
$null = Invoke-ResilientRemoteCall { Connect-MgGraph -Scopes $mgScopes -ContextScope Process } | |
} | |
catch { | |
throw "Failed to connect with required scopes: $_" | |
} | |
} | |
} | |
# Get Temporary Access Pass configuration | |
try { | |
$tapConfig = Invoke-ResilientRemoteCall { | |
(Invoke-MgGraphRequest -Method GET -Uri '/v1.0/policies/authenticationMethodsPolicy' -ErrorAction Stop).authenticationMethodConfigurations | Where-Object { $_.id -eq 'temporaryAccessPass' } | |
} | |
if ($tapConfig.state -ne 'enabled') { | |
throw "Temporary Access Pass authentication method is disabled for tenant $((Get-MgContext).TenantId)" | |
} | |
} | |
catch { | |
throw "Failed to retrieve TAP configuration: $_" | |
} | |
# Validate parameters | |
if ($StartDateTime -and ($StartDateTime.ToUniversalTime() -lt (Get-Date).ToUniversalTime().AddMinutes(1))) { | |
throw 'StartDateTime: Time cannot be in the past.' | |
} | |
if ($LifetimeInMinutes) { | |
if ($LifetimeInMinutes -gt $tapConfig.maximumLifetimeInMinutes) { | |
$LifetimeInMinutes = $tapConfig.maximumLifetimeInMinutes | |
Write-Warning "LifetimeInMinutes: Maximum lifetime capped at $LifetimeInMinutes minutes." | |
} | |
if ($LifetimeInMinutes -lt $tapConfig.minimumLifetimeInMinutes) { | |
$LifetimeInMinutes = $tapConfig.minimumLifetimeInMinutes | |
Write-Warning "LifetimeInMinutes: Minimum lifetime capped at $LifetimeInMinutes minutes." | |
} | |
} | |
if ($SendEmailToManager) { | |
if ([string]::IsNullOrEmpty($SenderEmailAddress)) { | |
if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { | |
try { | |
$SenderEmailAddress = Get-AutomationVariable -Name 'TAPConfig_SenderEmailAddress' -ErrorAction Stop | |
} | |
catch { | |
throw 'SenderEmailAddress is required when sending email to manager.' | |
} | |
} | |
else { | |
$SenderEmailAddress = (Get-MgContext).Account | |
Write-Verbose "Using authenticated user $SenderEmailAddress as sender email address" | |
} | |
} | |
if ('AzureAutomation/' -eq $env:AZUREPS_HOST_ENVIRONMENT -or $PSPrivateMetadata.JobId) { | |
if ([string]::IsNullOrEmpty($EmailLanguage)) { | |
try { | |
$EmailLanguage = Get-AutomationVariable -Name 'TAPConfig_EmailLanguage' -ErrorAction Stop | |
} | |
catch { | |
# Use default language | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailSubject)) { | |
try { | |
$EmailSubject = Get-AutomationVariable -Name 'TAPConfig_EmailSubject' -ErrorAction Stop | |
} | |
catch { | |
# Use default subject | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailTitle)) { | |
try { | |
$EmailTitle = Get-AutomationVariable -Name 'TAPConfig_EmailTitle' -ErrorAction Stop | |
} | |
catch { | |
# Use default title | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailSalutation)) { | |
try { | |
$EmailSalutation = Get-AutomationVariable -Name 'TAPConfig_EmailSalutation' -ErrorAction Stop | |
} | |
catch { | |
# Use default salutation | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailClosing)) { | |
try { | |
$EmailClosing = Get-AutomationVariable -Name 'TAPConfig_EmailClosing' -ErrorAction Stop | |
} | |
catch { | |
# Use default closing | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailTemplate)) { | |
try { | |
$EmailTemplate = Get-AutomationVariable -Name 'TAPConfig_EmailTemplate' -ErrorAction Stop | |
} | |
catch { | |
# Use default template | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailBodyText)) { | |
try { | |
$EmailBodyText = Get-AutomationVariable -Name 'TAPConfig_EmailBodyText' -ErrorAction Stop | |
} | |
catch { | |
# Use default template | |
} | |
} | |
if ([string]::IsNullOrEmpty($EmailImagesJson)) { | |
try { | |
$EmailImagesJson = Get-AutomationVariable -Name 'TAPConfig_EmailImagesJson' -ErrorAction Stop | |
} | |
catch { | |
# Use default images | |
} | |
} | |
if ([string]::IsNullOrEmpty($UseHtmlEmail)) { | |
try { | |
$rawValue = Get-AutomationVariable -Name 'TAPConfig_UseHtmlEmail' -ErrorAction Stop | |
if ($rawValue -is [bool]) { | |
$UseHtmlEmail = $rawValue | |
} | |
else { | |
try { | |
$UseHtmlEmail = [System.Convert]::ToBoolean($rawValue) | |
} | |
catch { | |
throw "Failed to convert TAPConfig_UseHtmlEmail value '$rawValue' to a boolean." | |
} | |
} | |
} | |
catch { | |
# Use default HTML email setting | |
} | |
} | |
} | |
} | |
# Get user information | |
try { | |
$select = 'id,userPrincipalName,mail,displayName,givenName,surname,employeeHireDate,userType,accountEnabled,usageLocation,preferredLanguage' | |
$expand = 'manager($select=id,userPrincipalName,mail,displayName,givenName,surname,preferredLanguage)' | |
$userObj = Invoke-ResilientRemoteCall { | |
Invoke-MgGraphRequest -Method GET ` | |
-Uri "/v1.0/users/$UserId`?`$select=$select&`$expand=$expand" ` | |
-ErrorAction Stop | |
} | |
if ($SendEmailToManager) { | |
try { | |
$userObj.mailboxLanguage = Invoke-ResilientRemoteCall { | |
Invoke-MgGraphRequest -Method GET ` | |
-Uri "/v1.0/users/$UserId/mailboxSettings/language" ` | |
-ErrorAction Stop | |
} | |
Write-Verbose "Retrieved mailbox language: $($userObj.mailboxLanguage.locale)" | |
} | |
catch { | |
Write-Warning "Failed to retrieve mailbox language: $_" | |
} | |
if ($null -ne $userObj.manager -and $null -ne $userObj.manager.id) { | |
try { | |
$userObj.manager.mailboxLanguage = Invoke-ResilientRemoteCall { | |
Invoke-MgGraphRequest -Method GET ` | |
-Uri "/v1.0/users/$($userObj.manager.id)/mailboxSettings/language" ` | |
-ErrorAction Stop | |
} | |
Write-Verbose "Retrieved manager mailbox language: $($userObj.manager.mailboxLanguage.locale)" | |
} | |
catch { | |
Write-Warning "Failed to retrieve manager mailbox language: $_" | |
} | |
} | |
} | |
} | |
catch { | |
throw "Failed to retrieve user information: $_" | |
} | |
if ($null -eq $userObj.manager -or -not $userObj.manager.id) { | |
Write-Error 'User ID does not have a manager.' | |
exit 1 | |
} | |
if ($SendEmailToManager) { | |
if ([string]::IsNullOrEmpty($userObj.manager.mail)) { | |
Write-Error "Cannot send email: Manager for $($userObj.displayName) does not have an email address." | |
exit 1 | |
} | |
} | |
# Build return data structure | |
$return.Data = [PSCustomObject]@{ | |
'@odata.context' = $userObj.'@odata.context' | |
Id = $userObj.id | |
UserPrincipalName = $userObj.userPrincipalName | |
GivenName = $userObj.givenName | |
Surname = $userObj.surname | |
Mail = $userObj.mail | |
DisplayName = $userObj.displayName | |
EmployeeHireDate = $userObj.employeeHireDate | |
UserType = $userObj.userType | |
AccountEnabled = $userObj.accountEnabled | |
UsageLocation = $userObj.usageLocation | |
PreferredLanguage = $userObj.mailboxLanguage.locale ?? $userObj.preferredLanguage | |
Manager = [PSCustomObject]@{ | |
Id = $userObj.manager.id | |
UserPrincipalName = $userObj.manager.userPrincipalName | |
Mail = $userObj.manager.mail | |
DisplayName = $userObj.manager.displayName | |
GivenName = $userObj.manager.givenName | |
Surname = $userObj.manager.surname | |
PreferredLanguage = $userObj.manager.mailboxLanguage.locale ?? $userObj.manager.preferredLanguage | |
} | |
AuthenticationMethods = [List[string]]::new() | |
} | |
# Validate user account state | |
if (-not $userObj.accountEnabled) { | |
Write-Error 'User ID is disabled.' | |
if ($SendEmailToManager) { | |
Send-EmailNotification -Reason UserBlocked ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
exit 1 | |
} | |
if ($userObj.userType -ne 'Member') { | |
Write-Error 'User ID needs to be of type Member.' | |
if ($SendEmailToManager) { | |
Send-EmailNotification -Reason UserBlocked ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
exit 1 | |
} | |
if ($null -ne $userObj.employeeHireDate) { | |
if ($userObj.employeeHireDate -ge (Get-Date).Date) { | |
Write-Error 'User ID has a future hire date.' | |
if ($SendEmailToManager) { | |
Send-EmailNotification -Reason UserBlocked ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
exit 1 | |
} | |
} | |
# Get user security groups | |
try { | |
$userGroupsResponse = Invoke-ResilientRemoteCall { | |
Invoke-MgGraphRequest -Method POST ` | |
-Uri "/v1.0/users/$($userObj.id)/getMemberGroups" ` | |
-Body @{ securityEnabledOnly = $true } | ConvertTo-Json -ErrorAction Stop | |
} | |
$userGroupIds = $userGroupsResponse.value | |
} | |
catch { | |
throw "Failed to retrieve user groups: $_" | |
} | |
# Check if user can use TAP based on group memberships | |
$isExcluded = $false | |
$isIncluded = $false | |
# Check exclusion rules | |
if ($null -ne $tapConfig.excludeTargets) { | |
$isExcluded = $tapConfig.excludeTargets | | |
Where-Object { $_.targetType -eq 'group' -and $_.id -in $userGroupIds } | | |
Select-Object -First 1 | |
} | |
# Check inclusion rules | |
if ($null -ne $tapConfig.includeTargets) { | |
$isIncluded = $tapConfig.includeTargets | | |
Where-Object { | |
$_.targetType -eq 'group' -and ($_.id -eq 'all_users' -or $_.id -in $userGroupIds) | |
} | | |
Select-Object -First 1 | |
} | |
if ($isExcluded -or -not $isIncluded) { | |
Write-Error "Authentication method 'Temporary Access Pass' is not enabled for this user ID." | |
if ($SendEmailToManager) { | |
Send-EmailNotification -Reason TAPNotEnabled ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
exit 1 | |
} | |
# Get user's authentication methods | |
try { | |
$authMethods = Invoke-ResilientRemoteCall { | |
Invoke-MgGraphRequest -Method GET ` | |
-Uri "/v1.0/users/$($userObj.id)/authentication/methods" ` | |
-ErrorAction Stop | |
} | |
} | |
catch { | |
throw "Failed to retrieve authentication methods: $_" | |
} | |
# Process authentication methods | |
foreach ($authMethod in $authMethods.value) { | |
if ($authMethod.'@odata.type' -match '^#microsoft\.graph\.(.+)AuthenticationMethod$') { | |
$methodType = $Matches[1] | |
$return.Data.AuthenticationMethods.Add($methodType) | |
if ($methodType -eq 'temporaryAccessPass') { | |
Write-Verbose "Found existing TAP Id $($authMethod.id)" | |
$return.Data | Add-Member -NotePropertyName 'TemporaryAccessPass' -NotePropertyValue $authMethod | |
$return.Data.TemporaryAccessPass | Add-Member -NotePropertyName 'Id' -NotePropertyValue $authMethod.id -Force | |
} | |
} | |
} | |
# Handle existing TAP and authentication methods | |
if ($return.Data.AuthenticationMethods.Count -gt 0) { | |
if ('temporaryAccessPass' -in $return.Data.AuthenticationMethods) { | |
# Check if TAP is the only authentication method besides password | |
$canDeleteTap = 'password' -in $return.Data.AuthenticationMethods -and $return.Data.AuthenticationMethods.Count -le 2 | |
if ($canDeleteTap) { | |
if ($return.Data.TemporaryAccessPass.methodUsabilityReason -ne 'Expired') { | |
Write-Warning 'A Temporary Access Pass code was already set before.' | |
} | |
if ($PSCmdlet.ShouldProcess( | |
"Delete existing Temporary Access Pass for $($userObj.userPrincipalName)", | |
"Do you confirm to remove the existing TAP for $($userObj.userPrincipalName) ?", | |
'Delete existing Temporary Access Pass' | |
)) { | |
try { | |
$null = Invoke-ResilientRemoteCall { | |
if (-not $WhatIfPreference) { | |
Invoke-MgGraphRequest -Method DELETE ` | |
-Uri "/v1.0/users/$($userObj.id)/authentication/temporaryAccessPassMethods/$($return.Data.TemporaryAccessPass.Id)" ` | |
-ErrorAction Stop | |
} | |
} | |
$return.Data.PSObject.Properties.Remove('TemporaryAccessPass') | |
} | |
catch { | |
throw "Failed to delete existing TAP: $_" | |
} | |
} | |
elseif ($WhatIfPreference) { | |
Write-Verbose 'What If: An existing Temporary Access Pass would have been deleted.' | |
} | |
else { | |
Write-Error 'Deletion of existing Temporary Access Pass was aborted.' | |
exit 1 | |
} | |
} | |
else { | |
# User has other authentication methods besides password and TAP | |
if ($return.Data.TemporaryAccessPass.methodUsabilityReason -eq 'Expired') { | |
$errorMsg = [StringBuilder]::new() | |
[void] $errorMsg.Append("An expired Temporary Access Pass code was found. `n") | |
[void] $errorMsg.Append('However, this process cannot be used to renew the Temporary Access Pass code because you have already configured other multi-factor authentication methods. ') | |
[void] $errorMsg.Append('Note that a Temporary Access Pass is only required during the initial onboarding process. ') | |
[void] $errorMsg.Append('You can then use your existing access to register additional methods, for example a security key. ') | |
[void] $errorMsg.Append('However, if you later lose access to all your multi-factor authentication methods, this self-service process cannot be used to recover. ') | |
[void] $errorMsg.Append('In this case, please contact the Global Service Desk who will help you reset your MFA methods.') | |
if ($SendEmailToManager) { | |
Send-EmailNotification -Reason TAPExpiredWithOtherMethodsConfigured ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
} | |
else { | |
$errorMsg = [StringBuilder]::new() | |
[void] $errorMsg.Append("An active Temporary Access Pass code has already been found. `n") | |
[void] $errorMsg.Append('It can only be displayed once after it has been created. ') | |
[void] $errorMsg.Append('As you have already configured other methods of multi-factor authentication, a new Temporary Access Pass can no longer be created via this self-service process. ') | |
[void] $errorMsg.Append('In this case, please contact the Global Service Desk who will help you reset your MFA methods.') | |
if ($SendEmailToManager) { | |
Send-EmailNotification -Reason TAPActiveWithOtherMethodsConfigured ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
} | |
Write-Error $errorMsg.ToString() | |
exit 1 | |
} | |
} | |
# Check if user has other authentication methods besides password | |
elseif ($return.Data.AuthenticationMethods.Count -gt 1 -or 'password' -notin $return.Data.AuthenticationMethods) { | |
$errorMsg = [StringBuilder]::new() | |
[void] $errorMsg.Append('This process cannot be used to request a Temporary Access Pass code as you have already configured other multi-factor authentication methods. ') | |
[void] $errorMsg.Append('Note that a Temporary Access Pass is only required during the initial onboarding process. ') | |
[void] $errorMsg.Append('You can then use your existing access to register additional methods, for example a security key. ') | |
[void] $errorMsg.Append('However, if you later lose access to all your multi-factor authentication methods, this self-service process cannot be used to recover. ') | |
[void] $errorMsg.Append('In this case, please contact the Global Service Desk who will help you reset your MFA methods.') | |
Write-Error $errorMsg.ToString() | |
if ($SendEmailToManager) { | |
Send-EmailNotification -Reason OtherMethodsConfigured ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
exit 1 | |
} | |
} | |
# Create new TAP if no existing one or it was deleted | |
if ($WhatIfPreference -or -not ($return.Data.PSObject.Properties['TemporaryAccessPass'])) { | |
$tapParams = @{} | |
if ($StartDateTime) { $tapParams.startDateTime = $StartDateTime.ToUniversalTime().ToString("o") } | |
if ($IsUsableOnce) { $tapParams.isUsableOnce = $true } | |
if ($LifetimeInMinutes) { $tapParams.lifetimeInMinutes = $LifetimeInMinutes } | |
if ($PSCmdlet.ShouldProcess( | |
"Create new Temporary Access Pass for $($userObj.userPrincipalName)", | |
"Do you confirm to create a new TAP for $($userObj.userPrincipalName) ?", | |
'New Temporary Access Pass' | |
)) { | |
try { | |
$tap = Invoke-ResilientRemoteCall { | |
if ($WhatIfPreference) { | |
Write-Verbose "What If: Would create a new TAP with parameters: $($tapParams | ConvertTo-Json -Compress)" | |
return $null | |
} | |
else { | |
Invoke-MgGraphRequest -Method POST ` | |
-Uri "/v1.0/users/$($userObj.id)/authentication/temporaryAccessPassMethods" ` | |
-Body ($tapParams | ConvertTo-Json) ` | |
-ErrorAction Stop | |
} | |
} | |
if ($tap) { | |
$return.Data | Add-Member -NotePropertyName 'TemporaryAccessPass' -NotePropertyValue $tap -Force | |
if ('temporaryAccessPass' -notin $return.Data.AuthenticationMethods) { | |
$return.Data.AuthenticationMethods.Add('temporaryAccessPass') | |
} | |
Write-Verbose 'A new Temporary Access Pass code was created.' | |
} | |
elseif ($WhatIfPreference) { | |
# Expected behavior in WhatIf mode | |
} | |
else { | |
throw "Failed to create TAP but no exception was thrown" | |
} | |
} | |
catch { | |
Write-Error "Failed to create new Temporary Access Pass: $_" | |
exit 1 | |
} | |
} | |
elseif ($WhatIfPreference) { | |
Write-Verbose "What If: A new Temporary Access Pass code would have been created with the following parameters:`n$(($tapParams | Out-String).TrimEnd())" | |
$return.WhatIf = @{ | |
returnCode = 0 | |
message = 'A Temporary Access Pass code may be created for this user ID.' | |
} | |
} | |
else { | |
Write-Error 'Creation of new Temporary Access Pass code was aborted.' | |
exit 1 | |
} | |
} | |
# Send email to manager | |
if ($SendEmailToManager) { | |
return Send-EmailNotification -Reason TAPCreated ` | |
-SenderEmailAddress $SenderEmailAddress ` | |
-EmailLanguage $EmailLanguage ` | |
-EmailSubject $EmailSubject ` | |
-EmailTitle $EmailTitle ` | |
-EmailSalutation $EmailSalutation ` | |
-EmailClosing $EmailClosing ` | |
-EmailBodyText $EmailBodyText ` | |
-EmailTemplate $EmailTemplate ` | |
-EmailImagesJson $EmailImagesJson ` | |
-UseHtmlEmail $UseHtmlEmail ` | |
-Data $return.Data | |
} | |
# Return results | |
if ($return.Data.PSObject.Properties.Count -eq 0) { | |
$return.Remove('Data') | |
} | |
if ($OutText) { | |
return $return.Data?.TemporaryAccessPass?.temporaryAccessPass ?? $null | |
} | |
if ($OutJson) { | |
return $return | ConvertTo-Json -Depth 4 | |
} | |
return $return |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment