From 856e7ac84f78201ae2e2dbdd3b581fc4734165ff Mon Sep 17 00:00:00 2001 From: jasonzqshen <30849413+jasonzqshen@users.noreply.github.com> Date: Tue, 5 Feb 2019 11:35:58 -0800 Subject: [PATCH] [Portal User Data GDRP] Add PS module for portal user data GDPR (#493) * Add PS module for portal user data GDPR * Support MFA * Update according to the comments * Update according to the comments --- .../Portal/PortalUserDataGdprUtilities.psm1 | 236 ++++++++++++++++++ Identity/AzureStack.Identity.Common.psm1 | 188 ++++++++++++++ Identity/GraphAPI/GraphAPI.psm1 | 31 +-- 3 files changed, 441 insertions(+), 14 deletions(-) create mode 100644 DatacenterIntegration/Portal/PortalUserDataGdprUtilities.psm1 create mode 100644 Identity/AzureStack.Identity.Common.psm1 diff --git a/DatacenterIntegration/Portal/PortalUserDataGdprUtilities.psm1 b/DatacenterIntegration/Portal/PortalUserDataGdprUtilities.psm1 new file mode 100644 index 00000000..6bb52431 --- /dev/null +++ b/DatacenterIntegration/Portal/PortalUserDataGdprUtilities.psm1 @@ -0,0 +1,236 @@ +<################################################### + # # + # Copyright (c) Microsoft. All rights reserved. # + # # + ##################################################> + +$DefaultAdminSubscriptionName = "Default Provider Subscription" + +<# +.Synopsis + Clear the portal user data +#> +function Clear-AzsUserData +{ + param + ( + # The directory tenant identifier of Azure Stack Administrator. + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] $AzsAdminDirectoryTenantId, + + # The Azure Stack ARM endpoint URI. + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [Uri] $AzsAdminArmEndpoint, + + # The user principal name of the account who's user data should be cleared. + [Parameter(Mandatory=$true)] + [ValidateNotNullOrEmpty()] + [string] $UserPrincipalName, + + # Optional: The directory tenant identifier of account who's user data should be cleared. + # If it is not specified, it will delete all the + [Parameter(Mandatory=$false)] + [ValidateNotNullOrEmpty()] + [string] $DirectoryTenantId, + + # Indicate whether it is ADFS env or not + [switch] $ADFS, + + # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials. + [ValidateNotNull()] + [pscredential] $AutomationCredential = $null + ) + #requires -Version 4.0 + #requires -Module "AzureRM.Profile" + #requires -Module "Azs.Subscriptions.Admin" + #requires -RunAsAdministrator + + $ErrorActionPreference = 'Stop' + $VerbosePreference = 'Continue' + + Import-Module $PSScriptRoot\..\..\Identity\GraphAPI\GraphAPI.psm1 -Force + Import-Module $PSScriptRoot\..\..\Identity\AzureStack.Identity.Common.psm1 -Force + + Write-Verbose "Login to Azure Stack Admin ARM..." -Verbose + $AzsAdminEnvironmentName = "AzureStackAdmin" + $adminArmEnv = Initialize-AzureRmEnvironment -AdminResourceManagerEndpoint $AzsAdminArmEndpoint -DirectoryTenantId $AzsAdminDirectoryTenantId -EnvironmentName $AzsAdminEnvironmentName + Write-Verbose "Created admin ARM env as $(ConvertTo-JSON $adminArmEnv)" -Verbose + + $params = @{ + AzureEnvironment = $adminArmEnv + SubscriptionName = $DefaultAdminSubscriptionName + } + if ($AutomationCredential) + { + $params.AutomationCredential = $AutomationCredential + } + $refreshToken = Initialize-AzureRmUserRefreshToken @params + Write-Verbose "Login into admin ARM and got the refresh token." -Verbose + + $adminSubscriptionId = (Get-AzureRmSubscription -Verbose | where { $_.Name -ieq $DefaultAdminSubscriptionName }).Id + Write-Verbose "Get default Admin subscription id $adminSubscriptionId." -Verbose + + if ($DirectoryTenantId) + { + $directoryTenantIdsArray = [string[]]$DirectoryTenantId + } + else + { + Write-Verbose "Input parameter 'DirectoryTenantId' is empty. Retrieving all the registered tenant directory..." -Verbose + $directoryTenantIdsArray = (Get-AzsDirectoryTenant -Verbose).TenantId + } + + Write-Host "Clearing the user data with input user principal name $UserPrincipalName and directory tenants '$DirectoryTenantIdsArray'..." + + $clearUserDataResults = @() # key is directory Id, value is clear response + + $initializeGraphEnvParams = @{ + RefreshToken = $refreshToken + } + if ($ADFS) + { + $initializeGraphEnvParams.AdfsFqdn = (New-Object Uri $adminArmEnv.ActiveDirectoryAuthority).Host + $initializeGraphEnvParams.GraphFqdn = (New-Object Uri $adminArmEnv.GraphUrl).Host + + $QueryParameters = @{ + '$filter' = "userPrincipalName eq '$($UserPrincipalName.ToLower())'" + } + } + else + { + $graphEnvironment = Resolve-GraphEnvironment -AzureEnvironment $adminArmEnv + Write-Verbose "Resolve the graph env as '$graphEnvironment '" -Verbose + $initializeGraphEnvParams.Environment = $graphEnvironment + + $QueryParameters = @{ + '$filter' = "userPrincipalName eq '$($UserPrincipalName.ToLower())' or startswith(userPrincipalName, '$($UserPrincipalName.Replace("@", "_").ToLower())')" + } + } + + foreach ($dirId in $directoryTenantIdsArray) + { + Write-Verbose "Intializing graph env..." -Verbose + Initialize-GraphEnvironment @initializeGraphEnvParams -DirectoryTenantId $dirId + Write-Verbose "Intialized graph env" -Verbose + + Write-Verbose "Querying all users..." -Verbose + $usersResponse = Invoke-GraphApi -ApiPath "/users" -QueryParameters $QueryParameters + Write-Verbose "Retrieved user object as $(ConvertTo-JSON $usersResponse.value)" -Verbose + + $userObjectId = $usersResponse.value.objectId + Write-Verbose "Retrieved user object Id as $userObjectId" -Verbose + if (-not $userObjectId) + { + Write-Warning "There is no user '$UserPrincipalName' under directory tenant Id $dirId." + $clearUserDataResult += [pscustomobject]@{ + DirectoryTenantId = $dirId + UserPrincipalName = $UserPrincipalName + ErrorMessage = "User not found in directory." + } + continue + } + elseif (([string[]]$userObjectId).Length -gt 1) + { + Write-Warning "There is one more users retrieved with '$UserPrincipalName' under directory tenant Id $dirId." + $clearUserDataResult += [pscustomobject]@{ + DirectoryTenantId = $dirId + UserPrincipalName = $UserPrincipalName + ErrorMessage = "One more user accounts found in directory. User principal name may be incorrect. " + } + continue + } + else + { + $params = @{ + AzsEnvironment = $adminArmEnv + UserObjectId = $userObjectId + DirectoryTenantId = $dirId + AdminSubscriptionId = $adminSubscriptionId + AzsAdminArmEndpoint = $AzsAdminArmEndpoint + } + $curResult = Clear-SinglePortalUserData @params + $clearUserDataResult += @( $curResult ) + } + } + + return $clearUserDataResult +} + +function Clear-SinglePortalUserData +{ + param + ( + # The user credential with which to acquire an access token targeting Graph. + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment] $AzsEnvironment, + + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [string] $UserObjectId, + + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [string] $DirectoryTenantId, + + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [string] $AdminSubscriptionId, + + # The Azure Stack ARM endpoint URI. + [Parameter(Mandatory=$true)] + [ValidateNotNull()] + [Uri] $AzsAdminArmEndpoint + ) + + try + { + Write-Verbose "Retrieving access token..." -Verbose + $accessToken = (Get-GraphToken -Resource $AzsEnvironment.ActiveDirectoryServiceEndpointResourceId -UseEnvironmentData).access_token + + $clearUserDataEndpoint = "$AzsAdminArmEndpoint/subscriptions/$AdminSubscriptionId/providers/Microsoft.PortalExtensionHost.Providers/ClearUserSettings?api-version=2017-09-01-preview" + $headers = @{ + Authorization = "Bearer $accessToken" + "Content-Type" = "application/json" + } + $payload = @{ + UserObjectId = $UserObjectId + DirectoryTenantId = $DirectoryTenantId + } + $httpPayload = ConvertTo-Json $payload -Depth 10 + Write-Verbose "Clearing user data with URI '$clearUserDataEndpoint' and payload: `r`n$httpPayload..." -Verbose + $clearUserDataResponse = $httpPayload | Invoke-RestMethod -Headers $headers -Method POST -Uri $clearUserDataEndpoint -TimeoutSec 120 -Verbose + + return [pscustomobject]@{ + DirectoryTenantId = $DirectoryTenantId + UserPrincipalName = $UserPrincipalName + ResponseData = $clearUserDataResponse + } + } + catch + { + if ($_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound -and (ConvertFrom-JSON $_.ErrorDetails.Message).error.code -eq "NoPortalUserData") + { + Write-Warning "No user data with user object Id and directory tenant Id" + return [pscustomobject]@{ + DirectoryTenantId = $DirectoryTenantId + UserPrincipalName = $UserPrincipalName + ErrorMessage = "No portal user data" + } + } + else + { + Write-Warning "Exception when clear user data with user object Id and directory tenant Id: $_`r`n$($_.Exception)" + return [pscustomobject]@{ + DirectoryTenantId = $DirectoryTenantId + UserPrincipalName = $UserPrincipalName + ErrorMessage = "Exception when clearing user data" + Exception = $_.Exception + } + } + } +} + +Export-ModuleMember -Function Clear-AzsUserData \ No newline at end of file diff --git a/Identity/AzureStack.Identity.Common.psm1 b/Identity/AzureStack.Identity.Common.psm1 new file mode 100644 index 00000000..14ea7b77 --- /dev/null +++ b/Identity/AzureStack.Identity.Common.psm1 @@ -0,0 +1,188 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# See LICENSE.txt in the project root for license information. + +<# +.Synopsis + Initialize the Azure RM environment +#> +function Initialize-AzureRmEnvironment +{ + [CmdletBinding()] + param + ( + # The endpoint of the Azure Stack Resource Manager service. + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [ValidateScript( {$_.Scheme -eq [System.Uri]::UriSchemeHttps})] + [uri] $ResourceManagerEndpoint, + + # The name of the home Directory Tenant in which the Azure Stack Administrator subscription resides. + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $DirectoryTenantId, + + # The specified name of this environment + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $EnvironmentName + ) + + $azureEnvironmentParams = @{ + Name = $environmentName + ARMEndpoint = $ResourceManagerEndpoint + } + $azureEnvironment = Add-AzureRmEnvironment @azureEnvironmentParams -ErrorAction Ignore + $azureEnvironment = Get-AzureRmEnvironment -Name $environmentName -ErrorAction Stop + return $azureEnvironment +} + +<# +.Synopsis + Initialize the Azure user account +#> +function Initialize-AzureRmUserAccount +{ + [CmdletBinding()] + param + ( + # The azure environment + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment] $AzureEnvironment, + + # The identifier of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription. + [ValidateNotNull()] + [string] $SubscriptionId = $null, + + # The display name of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription. + [ValidateNotNull()] + [string] $SubscriptionName = $null, + + # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials. + [Parameter()] + [ValidateNotNull()] + [pscredential] $AutomationCredential = $null + ) + + $params = @{ + EnvironmentName = $azureEnvironment.Name + } + if ($azureEnvironment.AdTenant) { + $params += @{ TenantId = $azureEnvironment.AdTenant } + } + if ($AutomationCredential) { + $params += @{ Credential = $AutomationCredential } + } + # Prompts the user for interactive login flow if automation credential is not specified + #$DebugPreference = "Continue" + Write-Verbose "Add azure RM account with parameters $(ConvertTo-JSON $params)" -Verbose + $azureAccount = Add-AzureRmAccount @params + if ($SubscriptionName) { + Select-AzureRmSubscription -SubscriptionName $SubscriptionName | Out-Null + } + elseif ($SubscriptionId) { + Select-AzureRmSubscription -SubscriptionId $SubscriptionId | Out-Null + } + return $azureAccount +} + +<# +.Synopsis + Initialize the Azure user account and get refresh token for the azure environment +#> +function Initialize-AzureRmUserRefreshToken +{ + [CmdletBinding()] + param + ( + # The azure environment + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment] $AzureEnvironment, + + # The identifier of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription. + [ValidateNotNull()] + [string] $SubscriptionId = $null, + + # The display name of the Administrator Subscription. If not specified, the script will attempt to use the set default subscription. + [ValidateNotNull()] + [string] $SubscriptionName = $null, + + # Optional: A credential used to authenticate with Azure Stack. Must support a non-interactive authentication flow. If not provided, the script will prompt for user credentials. + [Parameter()] + [ValidateNotNull()] + [pscredential] $AutomationCredential = $null + ) + + $params = @{ + AzureEnvironment = $AzureEnvironment + } + if ($SubscriptionId) + { + $params.SubscriptionId = $SubscriptionId + } + if ($SubscriptionName) + { + $params.SubscriptionName = $SubscriptionName + } + if ($AutomationCredential) + { + $params.AutomationCredential = $AutomationCredential + } + $azureStackAccount = Initialize-AzureRmUserAccount @params + + # Retrieve the refresh token + $tokens = @() + $tokens += try { [Microsoft.IdentityModel.Clients.ActiveDirectory.TokenCache]::DefaultShared.ReadItems() } catch {} + $tokens += try { [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.TokenCache.ReadItems() } catch {} + $refreshToken = $tokens | + Where Resource -EQ $AzureEnvironment.ActiveDirectoryServiceEndpointResourceId | + Where IsMultipleResourceRefreshToken -EQ $true | + Where DisplayableId -EQ $azureStackAccount.Context.Account.Id | + Sort ExpiresOn | + Select -Last 1 -ExpandProperty RefreshToken | + ConvertTo-SecureString -AsPlainText -Force + # Workaround due to regression in AzurePowerShell profile module which fails to populate the response object of "Add-AzureRmAccount" cmdlet + if (-not $refreshToken) { + if ($tokens.Count -eq 1) { + Write-Warning "Failed to find target refresh token from Azure PowerShell Cache; attempting to reuse the single cached auth context..." + $refreshToken = $tokens[0].RefreshToken | ConvertTo-SecureString -AsPlainText -Force + } + else { + throw "Unable to find refresh token from Azure PowerShell Cache. Please try the command again in a fresh PowerShell instance after running 'Clear-AzureRmContext -Scope CurrentUser -Force -Verbose'." + } + } + return $refreshToken +} + +<# +.Synopsis + Resolve the graph enviornment name +#> +function Resolve-GraphEnvironment +{ + [CmdletBinding()] + param + ( + # The azure environment + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [Microsoft.Azure.Commands.Profile.Models.PSAzureEnvironment] $AzureEnvironment + ) + + $graphEnvironment = switch ($AzureEnvironment.ActiveDirectoryAuthority) { + 'https://login.microsoftonline.com/' { 'AzureCloud' } + 'https://login.chinacloudapi.cn/' { 'AzureChinaCloud' } + 'https://login-us.microsoftonline.com/' { 'AzureUSGovernment' } + 'https://login.microsoftonline.de/' { 'AzureGermanCloud' } + Default { throw "Unsupported graph resource identifier: $_" } + } + return $graphEnvironment +} + +Export-ModuleMember -Function @( + "Initialize-AzureRmEnvironment", + "Initialize-AzureRmUserAccount", + "Initialize-AzureRmUserRefreshToken", + "Resolve-GraphEnvironment" +) diff --git a/Identity/GraphAPI/GraphAPI.psm1 b/Identity/GraphAPI/GraphAPI.psm1 index 53c829e6..40831d11 100644 --- a/Identity/GraphAPI/GraphAPI.psm1 +++ b/Identity/GraphAPI/GraphAPI.psm1 @@ -64,9 +64,13 @@ function Initialize-GraphEnvironment if ($AdfsFqdn) { - $Environment = 'ADFS' + $EnvironmentInternal = 'ADFS' Write-Warning "Parameters for ADFS have been specified; please note that only a subset of Graph APIs are available to be used in conjuction with ADFS." } + else + { + $EnvironmentInternal = $Environment + } if ($PromptForUserCredential) { @@ -75,15 +79,15 @@ function Initialize-GraphEnvironment if ($UserCredential) { - Write-Verbose "Initializing the module to use Graph environment '$Environment' for user '$($UserCredential.UserName)' in directory tenant '$DirectoryTenantId'." -Verbose + Write-Verbose "Initializing the module to use Graph environment '$EnvironmentInternal' for user '$($UserCredential.UserName)' in directory tenant '$DirectoryTenantId'." -Verbose } elseif ($RefreshToken) { - Write-Verbose "Initializing the module to use Graph environment '$Environment' (with refresh token) in directory tenant '$DirectoryTenantId'." -Verbose + Write-Verbose "Initializing the module to use Graph environment '$EnvironmentInternal' (with refresh token) in directory tenant '$DirectoryTenantId'." -Verbose } elseif ($ClientId -and $ClientCertificate) { - Write-Verbose "Initializing the module to use Graph environment '$Environment' for service principal '$($ClientId)' in directory tenant '$DirectoryTenantId' with certificate $($ClientCertificate.Thumbprint)." -Verbose + Write-Verbose "Initializing the module to use Graph environment '$EnvironmentInternal' for service principal '$($ClientId)' in directory tenant '$DirectoryTenantId' with certificate $($ClientCertificate.Thumbprint)." -Verbose } else { @@ -91,7 +95,7 @@ function Initialize-GraphEnvironment } $graphEnvironmentTemplate = @{} - $graphEnvironmentTemplate += switch ($Environment) + $graphEnvironmentTemplate += switch ($EnvironmentInternal) { 'AzureCloud' { @@ -217,7 +221,7 @@ function Initialize-GraphEnvironment IssuerTemplate = "https://$AdfsFqdn/adfs/{0}/" - LoginEndpoint = [Uri]"https://$AdfsFqdn/adfs/$DirectoryTenantId" + LoginEndpoint = [Uri]"https://$AdfsFqdn/adfs" GraphEndpoint = [Uri]"https://$GraphFqdn/$DirectoryTenantId" LoginBaseEndpoint = [Uri]"https://$AdfsFqdn/adfs/" @@ -230,13 +234,13 @@ function Initialize-GraphEnvironment default { - throw New-Object NotImplementedException("Unknown environment type '$Environment'") + throw New-Object NotImplementedException("Unknown environment type '$EnvironmentInternal'") } } # Note: if this data varies from environment to environment, declare it in switch above $graphEnvironmentTemplate += @{ - Environment = $Environment + Environment = $EnvironmentInternal DirectoryTenantId = $DirectoryTenantId User = [pscustomobject]@{ @@ -272,11 +276,6 @@ function Initialize-GraphEnvironment } } - if ($AdfsFqdn) - { - $graphEnvironmentTemplate.Applications = [pscustomobject]@{} - } - $Script:GraphEnvironment = [pscustomobject]$graphEnvironmentTemplate Write-Verbose "Graph Environment initialized: client-request-id: $($Script:GraphEnvironment.User.ClientRequestId)" -Verbose @@ -494,9 +493,13 @@ function Update-GraphAccessToken $response = Get-GraphToken -UseEnvironmentData $Script:GraphEnvironment.User.AccessToken = $response.access_token - $Script:GraphEnvironment.User.RefreshToken = if ($response.refresh_token) { ConvertTo-SecureString $response.refresh_token -AsPlainText -Force } else { $null } $Script:GraphEnvironment.User.AccessTokenUpdateTime = [DateTime]::UtcNow $Script:GraphEnvironment.User.AccessTokenExpiresIn = $response.expires_in + + if ($response.refresh_token) + { + $Script:GraphEnvironment.User.RefreshToken = ConvertTo-SecureString $response.refresh_token -AsPlainText -Force + } } <#