Configurations-OnPrem/ArcGISPortal.ps1

Configuration ArcGISPortal
{
    param(
        [Parameter(Mandatory=$True)]
        [System.String]
        $Version,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [System.Management.Automation.PSCredential]
        $ServiceCredential,

        [Parameter(Mandatory=$false)]
        [System.Boolean]
        $ForceServiceCredentialUpdate = $false,

        [Parameter(Mandatory=$false)]
        [System.Boolean]
        $ServiceCredentialIsDomainAccount = $false,

        [Parameter(Mandatory=$false)]
        [System.Boolean]
        $ServiceCredentialIsMSA = $false,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullorEmpty()]
        [System.Management.Automation.PSCredential]
        $PortalAdministratorCredential,

        [Parameter(Mandatory=$False)]
        [System.String]
        $PrimaryPortalMachine,

        [Parameter(Mandatory=$False)]
        [System.String]
        $ContentDirectoryLocation,

        [Parameter(Mandatory=$False)]
        [System.String]
        $AdminEmail,

        [Parameter(Mandatory=$False)]
        [System.String]
        $AdminFullName,

        [Parameter(Mandatory=$False)]
        [System.String]
        $AdminDescription,

        [Parameter(Mandatory=$False)]
        [System.Byte]
        $AdminSecurityQuestionIndex,
        
        [Parameter(Mandatory=$False)]
        [System.String]
        $AdminSecurityAnswer,

        [Parameter(Mandatory=$False)]
        [System.String]
        $LicenseFilePath,

        [Parameter(Mandatory=$False)]
        [System.String]
        $UserLicenseTypeId,

        [Parameter(Mandatory=$False)]
        [ValidateSet("AzureFiles","AzureBlob","AWSS3DynamoDB")]
        [AllowNull()] 
        [System.String]
        $CloudStorageType,

        [Parameter(Mandatory=$False)]
        [ValidateSet("AccessKey","ServicePrincipal","UserAssignedIdentity","SASToken")]
        [AllowNull()] 
        $ContentStoreAzureBlobAuthenticationType,

        [Parameter(Mandatory=$False)]
        [System.String]
        $ContentStoreAzureBlobUserAssignedIdentityId = $null,

        [Parameter(Mandatory=$False)]
        [System.String]
        $ContentStoreAzureBlobServicePrincipalTenantId = $null,

        [Parameter(Mandatory=$False)]
        [System.Management.Automation.PSCredential]
        $ContentStoreAzureBlobServicePrincipalCredentials = $null,

        [Parameter(Mandatory=$False)]
        [System.String]
        $ContentStoreAzureBlobServicePrincipalAuthorityHost = $null,

        [System.String]
        $AzureFileShareName,

        [System.String]
        $CloudNamespace,

        [System.String]
        $AWSRegion,

        [Parameter(Mandatory=$False)]
        [System.Management.Automation.PSCredential]
        $CloudStorageCredentials,

        [Parameter(Mandatory=$False)]
        [System.Boolean]
        $EnableHSTS = $False,

        [Parameter(Mandatory=$False)]
        [System.Boolean]
        $UsesSSL = $False,
        
        [Parameter(Mandatory=$False)]
        [System.Boolean]
        $DebugMode = $False
    )

    Import-DscResource -ModuleName PSDesiredStateConfiguration
    Import-DscResource -ModuleName ArcGIS -ModuleVersion 4.4.0 -Name ArcGIS_xFirewall, ArcGIS_Portal, ArcGIS_Service_Account, ArcGIS_WaitForComponent, ArcGIS_Portal_TLS

    if($null -ne $CloudStorageType)
    {
        if($CloudStorageType -ieq 'AWSS3DynamoDB') {
            $ContentDirectoryCloudConnectionString = "NAMESPACE=$($CloudNamespace);REGION=$($AWSRegion);"
            if($null -ne $CloudStorageCredentials){
                $ContentDirectoryCloudConnectionString += "ACCESS_KEY_ID=$($CloudStorageCredentials.UserName);SECRET_KEY=$($CloudStorageCredentials.GetNetworkCredential().Password)"
            }
        }else{
            if($null -ne $CloudStorageCredentials){
                $AccountName = $CloudStorageCredentials.UserName
                $EndpointSuffix = ''
                $Pos = $CloudStorageCredentials.UserName.IndexOf('.blob.')
                if($Pos -gt -1) {
                    $AccountName = $CloudStorageCredentials.UserName.Substring(0, $Pos)
                    $EndpointSuffix = $CloudStorageCredentials.UserName.Substring($Pos + 6) # Remove the hostname and .blob. suffix to get the storage endpoint suffix
                    $EndpointSuffix = ";EndpointSuffix=$($EndpointSuffix)"
                }
        
                if($CloudStorageType -ieq 'AzureFiles') {
                    $AzureFilesEndpoint = if($Pos -gt -1){$CloudStorageCredentials.UserName.Replace('.blob.','.file.')}else{$CloudStorageCredentials.UserName}
                    $AzureFileShareName = $AzureFileShareName.ToLower() # Azure file shares need to be lower case
                    $ContentDirectoryLocation = "\\$($AzureFilesEndpoint)\$AzureFileShareName\$($CloudNamespace)\portal\content"    
                } else {
                    $ContentDirectoryCloudContainerName = "arcgis-portal-content-$($CloudNamespace)portal"
                    $ContentDirectoryCloudConnectionString = "DefaultEndpointsProtocol=https;AccountName=$($AccountName)$($EndpointSuffix)"
                    if($ContentStoreAzureBlobAuthenticationType -ieq 'ServicePrincipal'){
                        $ClientSecret = $ContentStoreAzureBlobServicePrincipalCredentials.GetNetworkCredential().Password
                        $ContentDirectoryCloudConnectionString += ";tenantId=$($ContentStoreAzureBlobServicePrincipalTenantId);clientId=$($ContentStoreAzureBlobServicePrincipalCredentials.Username);clientSecret=$($ClientSecret);CredentialType=servicePrincipal"
                        if($ContentStoreAzureBlobServicePrincipalAuthorityHost -ne $null){
                            $ContentDirectoryCloudConnectionString += ";authorityHost=$($ContentStoreAzureBlobServicePrincipalAuthorityHost)"
                        }
                    }elseif($ContentStoreAzureBlobAuthenticationType -ieq 'UserAssignedIdentity'){
                        $ContentDirectoryCloudConnectionString += ";managedIdentityClientId=$($ContentStoreAzureBlobUserAssignedIdentityId);CredentialType=userAssignedIdentity"
                    }elseif($ContentStoreAzureBlobAuthenticationType -ieq 'SASToken'){
                        $SASToken = $CloudStorageCredentials.GetNetworkCredential().Password
                        $ContentDirectoryCloudConnectionString += ";sasToken=$($SASToken);CredentialType=sasToken"
                    }else{
                        $AccountKey = $CloudStorageCredentials.GetNetworkCredential().Password
                        $ContentDirectoryCloudConnectionString += ";AccountKey=$($AccountKey);CredentialType=accessKey"
                    }
                }
            }
        }
    }


    Node $AllNodes.NodeName
    {
        if($Node.Thumbprint){
            LocalConfigurationManager
            {
                CertificateId = $Node.Thumbprint
            }
        }
        
        $IsMultiMachinePortal = (($AllNodes | Measure-Object).Count -gt 1)
        
        $Depends = @()
        ArcGIS_xFirewall Portal_FirewallRules
        {
            Name                  = "PortalforArcGIS" 
            DisplayName           = "Portal for ArcGIS" 
            DisplayGroup          = "Portal for ArcGIS" 
            Ensure                = 'Present'
            Access                = "Allow" 
            State                 = "Enabled" 
            Profile               = ("Domain","Private","Public")
            LocalPort             = ("7080","7443","7654")                         
            Protocol              = "TCP" 
        }
        $Depends += @('[ArcGIS_xFirewall]Portal_FirewallRules')
        
        if($IsMultiMachinePortal) 
        {
            ArcGIS_xFirewall Portal_Database_OutBound
            {
                Name                  = "PortalforArcGIS-Outbound" 
                DisplayName           = "Portal for ArcGIS Outbound" 
                DisplayGroup          = "Portal for ArcGIS Outbound" 
                Ensure                = 'Present' 
                Access                = "Allow" 
                State                 = "Enabled" 
                Profile               = ("Domain","Private","Public")
                RemotePort            = ("7120","7220", "7005", "7099", "7199", "5701", "5702", "5703")  # Elastic Search uses 7120,7220 and Postgres uses 7654 for replication, Hazelcast uses 5701 and 5702 (extra 2 ports for situations where unable to get port)
                Direction             = "Outbound"                       
                Protocol              = "TCP" 
            }  
            $Depends += @('[ArcGIS_xFirewall]Portal_Database_OutBound')
            
            ArcGIS_xFirewall Portal_Database_InBound
            {
                Name                  = "PortalforArcGIS-Inbound" 
                DisplayName           = "Portal for ArcGIS Inbound" 
                DisplayGroup          = "Portal for ArcGIS Inbound" 
                Ensure                = 'Present' 
                Access                = "Allow" 
                State                 = "Enabled" 
                Profile               = ("Domain","Private","Public")
                LocalPort             = ("7120","7220","5701", "5702", "5703")  # Elastic Search uses 7120,7220, Hazelcast uses 5701 and 5702
                Protocol              = "TCP" 
            }  
            $Depends += @('[ArcGIS_xFirewall]Portal_Database_InBound')
            
            $VersionArray = $Version.Split('.')
            if($VersionArray[0] -ieq 11 -and $VersionArray -ge 3){ # 11.3 or later
                ArcGIS_xFirewall Portal_Ignite_OutBound
                {
                    Name                  = "PortalforArcGIS-Ignite-Outbound" 
                    DisplayName           = "Portal for ArcGIS Ignite Outbound" 
                    DisplayGroup          = "Portal for ArcGIS Ignite Outbound" 
                    Ensure                = 'Present' 
                    Access                = "Allow" 
                    State                 = "Enabled" 
                    Profile               = ("Domain","Private","Public")
                    RemotePort            = ("7820","7830", "7840") # Ignite uses 7820,7830,7840
                    Direction             = "Outbound"                       
                    Protocol              = "TCP" 
                }  
                $Depends += @('[ArcGIS_xFirewall]Portal_Ignite_OutBound')
                
                ArcGIS_xFirewall Portal_Ignite_InBound
                {
                    Name                  = "PortalforArcGIS-Ignite-Inbound" 
                    DisplayName           = "Portal for ArcGIS Ignite Inbound" 
                    DisplayGroup          = "Portal for ArcGIS Ignite Inbound" 
                    Ensure                = 'Present' 
                    Access                = "Allow" 
                    State                 = "Enabled" 
                    Profile               = ("Domain","Private","Public")
                    RemotePort            = ("7820","7830", "7840") # Ignite uses 7820,7830,7840
                    Protocol              = "TCP" 
                }  
                $Depends += @('[ArcGIS_xFirewall]Portal_Ignite_InBound')
            }
        }

        $DataDirsForPortal = @('HKLM:\SOFTWARE\ESRI\Portal for ArcGIS')
        if($ContentDirectoryLocation -and (-not($ContentDirectoryLocation.StartsWith('\'))) -and ($CloudStorageType -ne 'AzureFiles'))
        {
            $DataDirsForPortal += $ContentDirectoryLocation
            $DataDirsForPortal += (Split-Path $ContentDirectoryLocation -Parent)

            File ContentDirectoryLocation
            {
                Ensure = "Present"
                DestinationPath = $ContentDirectoryLocation
                Type = 'Directory'
                DependsOn = $Depends
            }  
            $Depends += "[File]ContentDirectoryLocation"
        }

        ArcGIS_Service_Account Portal_RunAs_Account
        {
            Name            = 'Portal for ArcGIS'
            RunAsAccount    = $ServiceCredential
            ForceRunAsAccountUpdate = $ForceServiceCredentialUpdate
            SetStartupToAutomatic = $True
            Ensure          = "Present"
            DataDir         = $DataDirsForPortal
            DependsOn       = $Depends
            IsDomainAccount = $ServiceCredentialIsDomainAccount
            IsMSAAccount    = $ServiceCredentialIsMSA
        }
        
        $Depends += @('[ArcGIS_Service_Account]Portal_RunAs_Account')

        if(-not($ServiceCredentialIsMSA) -and $AzureFilesEndpoint -and $CloudStorageCredentials -and ($CloudStorageType -ieq 'AzureFiles')) 
        {
            $FilesStorageAccountName = $AzureFilesEndpoint.Substring(0, $AzureFilesEndpoint.IndexOf('.'))
            $StorageAccountKey       = $CloudStorageCredentials.GetNetworkCredential().Password
      
            Script PersistStorageCredentials
            {
                TestScript = { 
                                $result = cmdkey "/list:$using:AzureFilesEndpoint"
                                $result | ForEach-Object{Write-verbose -Message "cmdkey: $_" -Verbose}
                                if($result -like '*none*')
                                {
                                    return $false
                                }
                                return $true
                            }
                SetScript = { 
                                $result = cmdkey "/add:$using:AzureFilesEndpoint" "/user:$using:FilesStorageAccountName" "/pass:$using:StorageAccountKey" 
                                $result | ForEach-Object{Write-verbose -Message "cmdkey: $_" -Verbose}
                            }
                GetScript            = { return @{} }                  
                DependsOn            = $Depends
                PsDscRunAsCredential = $ServiceCredential # This is critical, cmdkey must run as the service account to persist property
            }              
            $Depends += '[Script]PersistStorageCredentials'

            $RootPathOfFileShare = "\\$($AzureFilesEndpoint)\$AzureFileShareName"
            Script CreatePortalContentFolder
            {
                TestScript = { 
                                Test-Path $using:ContentDirectoryLocation
                            }
                SetScript = {                   
                                Write-Verbose "Mount to $using:RootPathOfFileShare"
                                $DriveInfo = New-PSDrive -Name 'Z' -PSProvider FileSystem -Root $using:RootPathOfFileShare
                                if(-not(Test-Path $using:ContentDirectoryLocation)) {
                                    Write-Verbose "Creating folder $using:ContentDirectoryLocation"
                                    New-Item $using:ContentDirectoryLocation -ItemType directory
                                }else {
                                    Write-Verbose "Folder '$using:ContentDirectoryLocation' already exists"
                                }
                            }
                GetScript            = { return @{} }     
                DependsOn            = $Depends
                PsDscRunAsCredential = $ServiceCredential # This is important, only arcgis account has access to the file share on AFS
            }             
            $Depends += '[Script]CreatePortalContentFolder'
        }else{
            Write-Verbose "For MSA we assume these steps have been done independent of the module."
        }

        if($Node.NodeName -ine $PrimaryPortalMachine)
        {
            if($UsesSSL){
                ArcGIS_WaitForComponent "WaitForPortal$($PrimaryPortalMachine)"{
                    Component = "Portal"
                    InvokingComponent = "Portal"
                    ComponentHostName = $PrimaryPortalMachine
                    ComponentContext = "arcgis"
                    Credential = $PortalAdministratorCredential
                    Ensure = "Present"
                    RetryIntervalSec = 60
                    RetryCount = 100
                }
                $Depends += "[ArcGIS_WaitForComponent]WaitForPortal$($PrimaryPortalMachine)"
            }else{
                WaitForAll "WaitForAllPortal$($PrimaryPortalMachine)"{
                    ResourceName = "[ArcGIS_Portal]Portal$($PrimaryPortalMachine)"
                    NodeName = $PrimaryPortalMachine
                    RetryIntervalSec = 60
                    RetryCount = 90
                    DependsOn = $Depends
                }
                $Depends += "[WaitForAll]WaitForAllPortal$($PrimaryPortalMachine)"
            }
        }   
               
        ArcGIS_Portal "Portal$($Node.NodeName)"
        {
            Ensure = 'Present'
            Version = $Version
            PortalHostName = $Node.NodeName
            LicenseFilePath = $LicenseFilePath
            UserLicenseTypeId = $UserLicenseTypeId
            PortalAdministrator = $PortalAdministratorCredential 
            AdminEmail = $AdminEmail
            AdminFullName = $AdminFullName
            AdminDescription = $AdminDescription
            AdminSecurityQuestionIndex = $AdminSecurityQuestionIndex
            AdminSecurityAnswer = $AdminSecurityAnswer
            ContentDirectoryLocation = $ContentDirectoryLocation
            Join = if($Node.NodeName -ine $PrimaryPortalMachine) { $true } else { $false } 
            IsHAPortal = if($IsMultiMachinePortal){ $true } else { $false }
            PeerMachineHostName = if($Node.NodeName -ine $PrimaryPortalMachine) { $PrimaryPortalMachine } else { "" } #add peer machine name
            EnableDebugLogging = if($DebugMode) { $true } else { $false }
            LogLevel = if($DebugMode) { 'DEBUG' } else { 'WARNING' }
            ContentDirectoryCloudConnectionString = $ContentDirectoryCloudConnectionString                            
            ContentDirectoryCloudContainerName = $ContentDirectoryCloudContainerName
            EnableCreateSiteDebug = if($DebugMode) { $true } else { $false }
            DependsOn =  $Depends
        }
        $Depends += "[ArcGIS_Portal]Portal$($Node.NodeName)"
        
        ArcGIS_Portal_TLS ArcGIS_Portal_TLS
        {
            PortalHostName          = $Node.NodeName
            SiteAdministrator       = $PortalAdministratorCredential
            WebServerCertificateAlias =  if($Node.SSLCertificate){$Node.SSLCertificate.CName}else{$null}
            CertificateFileLocation = if($Node.SSLCertificate){$Node.SSLCertificate.Path}else{$null}
            CertificatePassword = if($Node.SSLCertificate){$Node.SSLCertificate.Password}else{$null}
            SslRootOrIntermediate = if($Node.SslRootOrIntermediate){$Node.SslRootOrIntermediate}else{$null}
            EnableHSTS = $EnableHSTS
            DependsOn               = $Depends
        }
    }   
}