Microsoft.AzureStackHCI.Management.psm1

#
# AzureStack HCI Registration and Unregistration Powershell Cmdlets.
#

$ErrorActionPreference = 'Stop'

$UsageServiceFirstPartyAppId = "1322e676-dee7-41ee-a874-ac923822781c"

function Setup-Logging{
param(
    [string] $LogFilePrefix
    )
    
    $date = Get-Date
    $datestring = "{0}{1:d2}{2:d2}-{3:d2}{4:d2}" -f $date.year,$date.month,$date.day,$date.hour,$date.minute
    $LogFileName = $LogFilePrefix + "_" + $datestring + ".log"

    Start-Transcript -LiteralPath $LogFileName -Append | out-null
}

function Get-ResourceId{
param(
    [string] $ResourceName,
    [string] $Subscription,
    [string] $ResourceGroupName
    )
    
    return "/subscriptions/" + $Subscription + "/resourceGroups/" + $ResourceGroupName + "/providers/Microsoft.AzureStackHCI/clusters/" + $ResourceName
}

function Check-UsageAppRoles{
param(
    [string] $AppId
    )
    
    Write-Host "Checking admin consent status for AAD Application" $AppId
    $usagesp = Get-AzureADServicePrincipal -Filter "AppId eq '$UsageServiceFirstPartyAppId'"
    $usageWritePermission = $usagesp.AppRoles| Where-Object {$_.Value -contains "Cluster.Usage.Write"}
    $metadataWritePermission = $usagesp.AppRoles| Where-Object {$_.Value -contains "Cluster.Metadata.Write"}
    $appSP = Get-AzureADServicePrincipal -Filter "AppId eq '$AppId'"
    $assignedPerms = Get-AzureADServiceAppRoleAssignedTo -ObjectId $appSP.ObjectId
    $usageWrite = $assignedPerms | where { ($_.Id -eq $usageWritePermission.Id) }
    $metadataWrite = $assignedPerms | where { ($_.Id -eq $metadataWritePermission.Id) }

    if($usageWrite -ne $Null -and $metadataWrite -ne $Null) # Check both Usage.Write and Metadata.Write are in consented state.
    {
        if($usageWrite.DeletionTimestamp -eq $Null -or ($usageWrite.DeletionTimestamp -lt $usageWrite.CreationTimestamp))
        {
            if($metadataWrite.DeletionTimestamp -eq $Null -or ($metadataWrite.DeletionTimestamp -lt $metadataWrite.CreationTimestamp))
            {
                return $True
            }
        }
    }

    return $false
}

function Create-Application{
param(
    [string] $AppName
    )
    
    Write-Host "Creating AAD Application" $AppName
    $usagesp = Get-AzureADServicePrincipal -Filter "AppId eq '$UsageServiceFirstPartyAppId'"
    $usageWritePermission = $usagesp.AppRoles| Where-Object {$_.Value -contains "Cluster.Usage.Write"}
    $metadataWritePermission = $usagesp.AppRoles| Where-Object {$_.Value -contains "Cluster.Metadata.Write"}

    $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] 

    $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess
    $requiredAccess.ResourceAppId = $usagesp.AppId
    $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess]  

    $usageWriteAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess
    $usageWriteAccess.Type = "Role"
    $usageWriteAccess.Id = $usageWritePermission.Id 
    $requiredAccess.ResourceAccess.Add($usageWriteAccess)

    $metadataWriteAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess
    $metadataWriteAccess.Type = "Role"
    $metadataWriteAccess.Id = $metadataWritePermission.Id 
    $requiredAccess.ResourceAccess.Add($metadataWriteAccess)

    $requiredResourcesAccess.Add($requiredAccess)

    # Create application
    $app = New-AzureADApplication -DisplayName $AppName -RequiredResourceAccess $requiredResourcesAccess
    $sp = New-AzureADServicePrincipal -AppId $app.AppId

    Write-Host "Created new AAD Application" $app.AppId

    return $app.AppId
}

function Grant-AdminConsent{
param(
    [string] $AppId
    )
    
    Write-Host "Granting admin consent for AAD Application Id" $AppId
    $usagesp = Get-AzureADServicePrincipal -SearchString "Microsoft.AzureStackHCI Usage Service"
    $usageWritePermission = $usagesp.AppRoles| Where-Object {$_.Value -contains "Cluster.Usage.Write"}
    $metadataWritePermission = $usagesp.AppRoles| Where-Object {$_.Value -contains "Cluster.Metadata.Write"}
    $appSP = Get-AzureADServicePrincipal -Filter "AppId eq '$AppId'"
    
    try 
    {
        New-AzureADServiceAppRoleAssignment -ObjectId $appSP.ObjectId -PrincipalId $appSP.ObjectId -ResourceId $usagesp.ObjectId -Id $metadataWritePermission.Id 
        New-AzureADServiceAppRoleAssignment -ObjectId $appSP.ObjectId -PrincipalId $appSP.ObjectId -ResourceId $usagesp.ObjectId -Id $usageWritePermission.Id 
    }
    catch 
    {
        Write-Host "Exception occured when granting admin consent"
        $ErrorMessage = $_.Exception.Message
        Write-Host $ErrorMessage
        return $False
    }

    return $True
}

function Azure-Login{
param(
    [string] $Subscription,
    [string] $TenantId,
    [string] $ArmAccessToken,
    [string] $GraphAccessToken,
    [string] $AccountId,
    [bool]   $IsDogfood
    )

    Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
    Install-Module -Name Az.Accounts -Force -AllowClobber
    Install-Module -Name Az.Resources -Force -AllowClobber
    Install-Module -Name AzureAD -Force -AllowClobber

    if($IsDogfood)
    {
        $azEnv = Add-AzEnvironment -Name Dogfood -PublishSettingsFileUrl "https://windows.azure-test.net/publishsettings/index" -ServiceEndpoint "https://management-preview.core.windows-int.net/" -ManagementPortalUrl "https://windows.azure-test.net/" -ActiveDirectoryEndpoint "https://login.windows-ppe.net/" -ActiveDirectoryServiceEndpointResourceId "https://management.core.windows.net/" -ResourceManagerEndpoint "https://api-dogfood.resources.windows-int.net/" -GalleryEndpoint "https://df.gallery.azure-test.net/" -GraphEndpoint "https://graph.ppe.windows.net/" -GraphAudience "https://graph.ppe.windows.net/"

        if([string]::IsNullOrEmpty($ArmAccessToken) -or [string]::IsNullOrEmpty($GraphAccessToken) -or [string]::IsNullOrEmpty($AccountId))
        {
            $azProfile = Connect-AzAccount -Environment Dogfood -TenantId $TenantId -Subscription $Subscription -Scope Process
            $azAD = Connect-AzureAD -AzureEnvironmentName AzurePPE -TenantId $TenantId
        }
        else
        {
            $azProfile = Connect-AzAccount -Environment Dogfood -TenantId $TenantId -Subscription $Subscription -AccessToken $ArmAccessToken -AccountId $AccountId -Scope Process
            $azAD = Connect-AzureAD -AzureEnvironmentName AzurePPE -TenantId $TenantId -AadAccessToken $GraphAccessToken -AccountId $AccountId
        }
    }
    else
    {
        if([string]::IsNullOrEmpty($ArmAccessToken) -or [string]::IsNullOrEmpty($GraphAccessToken) -or [string]::IsNullOrEmpty($AccountId))
        {
            $azProfile = Connect-AzAccount -TenantId $TenantId -Subscription $Subscription -Scope Process
            $azAD = Connect-AzureAD -TenantId $TenantId
        }
        else
        {
            $azProfile = Connect-AzAccount -TenantId $TenantId -Subscription $Subscription -AccessToken $ArmAccessToken -AccountId $AccountId -Scope Process
            $azAD = Connect-AzureAD -TenantId $TenantId -AadAccessToken $GraphAccessToken -AccountId $AccountId
        }
    }
}

enum OperationStatus
{
    Failed;
    Success;
    PendingForAdminConsent;
    Cancelled
}

function Register-AzureStackHCI{
param(
    [string] $Location,
    [string] $ResourceName,
    [string] $Subscription,
    [string] $TenantId,
    [string] $ResourceGroupName,
    [string] $ArmAccessToken,
    [string] $GraphAccessToken,
    [string] $AccountId,
    [bool]   $IsDogfood
    )

    try
    {
        Setup-Logging -LogFilePrefix "RegisterHCI"
    
        Write-Host "Register-AzureStackHCI triggered -" "Location:" $Location "ResourceName:" $ResourceName `
            "Subscription:" $Subscription "Tenant:" $TenantId "ResourceGroupName:" $ResourceGroupName "AccountId:" $AccountId "IsDogfood:" $IsDogfood

        Write-Progress -activity "Registering StackHCI with Azure" -status "0% Complete:" -percentcomplete 0
    
        Azure-Login -Subscription $Subscription -TenantId $TenantId -ArmAccessToken $ArmAccessToken -GraphAccessToken $GraphAccessToken -AccountId $AccountId -IsDogfood $IsDogfood

        Write-Progress -activity "Registering StackHCI with Azure" -status "5% Complete:" -percentcomplete 5
    
        $regRP = Register-AzResourceProvider -ProviderNamespace Microsoft.AzureStackHCI

        Write-Progress -activity "Registering StackHCI with Azure" -status "10% Complete:" -percentcomplete 10
    
        $resourceId = Get-ResourceId -ResourceName $ResourceName -Subscription $Subscription -ResourceGroupName $ResourceGroupName
        
        $resource = Get-AzResource -ResourceId $resourceId -ErrorAction Ignore

        if($resource -eq $Null)
        {
            # Create new application

            $appId = Create-Application -AppName $ResourceName

            Write-Host "Created AAD Application with Id" $appId

            Write-Progress -activity "Registering StackHCI with Azure" -status "25% Complete:" -percentcomplete 25

            # Create new resource by calling RP

            $resGroup = Get-AzResourceGroup -Name $ResourceGroupName -Location $Location -ErrorAction Ignore

            if($resGroup -eq $Null)
            {
                 $resGroup = New-AzResourceGroup -Name $ResourceGroupName -Location $Location
            }

            $properties = @{"aadClientId"="$appId";"aadTenantId"="$TenantId"}
            $resource = New-AzResource -ResourceId $resourceId -Location $Location -ApiVersion 2020-03-01-preview -PropertyObject $properties -Force

            Write-Progress -activity "Registering StackHCI with Azure" -status "40% Complete:" -percentcomplete 40

            # Try Granting admin consent for requested permissions

            $adminConsented = Grant-AdminConsent -AppId $appId

            if($adminConsented -eq $False)
            {
                return [OperationStatus]::PendingForAdminConsent
            }

            Write-Progress -activity "Registering StackHCI with Azure" -status "55% Complete:" -percentcomplete 55
        }
        else
        {
            # Resource and Application exists. Check admin consent status
            
            $appId = $resource.Properties.aadClientId

            $rolesPresent = Check-UsageAppRoles -AppId $appId
        
            Write-Progress -activity "Registering StackHCI with Azure" -status "35% Complete:" -percentcomplete 35

            if($rolesPresent -eq $False)
            {
                # Try Granting admin consent for requested permissions

                $adminConsented = Grant-AdminConsent -AppId $appId

                if($adminConsented -eq $False)
                {
                    return [OperationStatus]::PendingForAdminConsent
                }
            }

            Write-Progress -activity "Registering StackHCI with Azure" -status "55% Complete:" -percentcomplete 55
        }

        # At this point Application should be created and consented by admin.

        $appId = $resource.Properties.aadClientId
        $cloudId = $resource.Properties.cloudId 
        $app = Get-AzureADApplication -Filter "AppId eq '$appId'"
        $objectId = $app.ObjectId
        $appSP = Get-AzureADServicePrincipal -Filter "AppId eq '$appId'"
        $spObjectId = $appSP.ObjectId

        $RegContext = Get-HCIUsageRegistration

        if($RegContext.IsRegistered -eq $False)
        {
            # Add certificate

            $certBase64= New-HCIUsageCertificate
            $Cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]([System.Convert]::FromBase64String($CertBase64))
    
            Write-Progress -activity "Registering StackHCI with Azure" -status "70% Complete:" -percentcomplete 70

            $now = [System.DateTime]::UtcNow
            $appCredential = New-AzureADApplicationKeyCredential -ObjectId $objectId -Type AsymmetricX509Cert -Usage Verify -Value $CertBase64 -StartDate $now -EndDate $Cert.GetExpirationDateString()

            Write-Progress -activity "Registering StackHCI with Azure" -status "80% Complete:" -percentcomplete 80

            # Register by calling on-prem usage service Cmdlet

            $authority = "https://login.microsoftonline.com"
            $usageServiceApiScope = "https://azurestackhci-usage.azurewebsites.net/.default"
            $graphServiceApiScope = "https://graph.microsoft.com/.default"

            if($IsDogfood)
            {
                $authority = "https://login.windows-ppe.net"
                $usageServiceApiScope = "https://azurestackhci-usage-df.azurewebsites.net/.default"
                $graphServiceApiScope = "https://graph.ppe.windows.net/.default"
            }

            Register-HCIUsage -ServiceEndpoint "https://azurestackhci-usage-df.azurewebsites.net" `
                              -UsageServiceApiScope $usageServiceApiScope `
                              -GraphServiceApiScope $graphServiceApiScope `
                              -AADAuthority $authority `
                              -AppId $appId `
                              -TenantId $TenantId `
                              -CloudId $cloudId `
                              -SubscriptionId $Subscription `
                              -Certificate $certBase64 `
                              -ObjectId $objectId `
                              -ResourceName $ResourceName `
                              -ProviderNamespace "Microsoft.AzureStackHCI" `
                              -ResourceArmId $resourceId `
                              -ServicePrincipalClientId $spObjectId
        }

        Write-Progress -activity "Completed Registering with Azure" -Completed

        return [OperationStatus]::Success
    }
    finally
    {
        Stop-Transcript | out-null
    }
}

function Unregister-AzureStackHCI{
param(
    [string] $ResourceName,
    [string] $Subscription,
    [string] $TenantId,
    [string] $ResourceGroupName,
    [string] $ArmAccessToken,
    [string] $GraphAccessToken,
    [string] $AccountId,
    [bool]   $IsDogfood,
    [bool]   $Force
    )

    try
    {
        Setup-Logging -LogFilePrefix "UnregisterHCI"

        Write-Host "Unregister-AzureStackHCI triggered -" "ResourceName:" $ResourceName `
                   "Subscription:" $Subscription "Tenant:" $TenantId "ResourceGroupName:" $ResourceGroupName `
                   "AccountId:" $AccountId "IsDogfood:" $IsDogfood "Force:" $Force

    
        Write-Progress -activity "Unregistering StackHCI from Azure" -status "0% Complete:" -percentcomplete 0
    
        $confirmation = ''
    
        if($Force)
        {
            $confirmation = 'y'
        }
        else
        {
            $confirmation = Read-Host "Are you sure you want to proceed with Unregistration: Type y to proceed"
        }

        Write-Progress -activity "Unregistering StackHCI from Azure" -status "5% Complete:" -percentcomplete 5
    
        if ($confirmation -eq 'y')
        {
            $RegContext = Get-HCIUsageRegistration

            $resourceId = Get-ResourceId -ResourceName $ResourceName -Subscription $Subscription -ResourceGroupName $ResourceGroupName 
        
            Write-Progress -activity "Unregistering StackHCI from Azure" -status "10% Complete:" -percentcomplete 10
        
            Azure-Login -Subscription $Subscription -TenantId $TenantId -ArmAccessToken $ArmAccessToken -GraphAccessToken $GraphAccessToken -AccountId $AccountId -IsDogfood $IsDogfood
            
            Write-Progress -activity "Unregistering StackHCI from Azure" -status "30% Complete:" -percentcomplete 30
        
            if($RegContext.IsRegistered -eq $True)
            {
                Unregister-HCIUsage
            }
        
            Write-Progress -activity "Unregistering StackHCI from Azure" -status "50% Complete:" -percentcomplete 50
        
            $resource = Get-AzResource -ResourceId $resourceId -ErrorAction Ignore

            if($resource -ne $Null)
            {
                $appId = $resource.Properties.aadClientId
                $app = Get-AzureADApplication -Filter "AppId eq '$appId'"
                
                if($app -ne $Null)
                {
                    Remove-AzureADApplication -ObjectId $app.ObjectId
                }

                Write-Progress -activity "Unregistering StackHCI from Azure" -status "70% Complete:" -percentcomplete 70

                $remResource = Remove-AzResource -ResourceId $resourceId -Force
            }
            
            Write-Progress -activity "Unregistering StackHCI from Azure" -status "95% Complete:" -percentcomplete 95
        }
        else
        {
            return [OperationStatus]::Cancelled
        }    

        Write-Progress -activity "Completed Unregistering with Azure" -Completed
    
        return [OperationStatus]::Success
    }
    finally
    {
        Stop-Transcript | out-null
    }
}

Export-ModuleMember -Function Register-AzureStackHCI
Export-ModuleMember -Function Unregister-AzureStackHCI