Configuring the Windows 10 Pro Lock Screen using MEM

Windows 10 Enterprise supports a specific MEM policy to configure the Windows 10 Lock screen for End-users. If you’re unlucky enough to be on a lesser Windows 10 version, you’ll need to trick the OS into thinking the lock screen is modified by the user instead of through a policy.

Here’s a simple ARM template for blob storage and a PS script to deploy through MEM in user context to configure the lock screen of your users:

1-click ARM template

And the script itself (don’t forget to configure the image URL):

<#
    .SYNOPSIS
    Sets custom lock screen based on file in an Azure Storage Blob container
    See blob template to automatically configure a blob container: https://gitlab.com/Lieben/assortedFunctions/-/blob/master/ARM%20templates/blob%20storage%20with%20container%20for%20Teams%20Backgrounds%20and%20public%20access.json
   
    .NOTES
    filename: set-windows10LockScreen.ps1
    author: Jos Lieben
    blog: www.lieben.nu
    created: 13/05/2021
#>

$changedDate = "2021-05-13"
$lockscreenFileURL = "https://tasdsadgsadsad.blob.core.windows.net/teamsbackgrounds/figure-a.jpg" #this is the full URL to the desired lock screen image

Start-Transcript -Path (Join-Path -Path $Env:TEMP -ChildPath "set-windows10LockScreen.log")

$tempFile = (Join-Path $Env:TEMP -ChildPath "img100.jpg")

try{
    Write-Output "downloading lock screen file from $lockscreenFileURL"
    Invoke-WebRequest -Uri $lockscreenFileURL -UseBasicParsing -Method GET -OutFile $tempFile
    Write-Output "file downloaded to $tempFile"
}catch{
    Write-Output "Failed to download file, aborting"
    Write-Error $_ -ErrorAction SilentlyContinue
    Exit
}

[Windows.System.UserProfile.LockScreen,Windows.System.UserProfile,ContentType=WindowsRuntime] | Out-Null
Add-Type -AssemblyName System.Runtime.WindowsRuntime

$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
Function Await($WinRtTask, $ResultType) {
    $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
    $netTask = $asTask.Invoke($null, @($WinRtTask))
    $netTask.Wait(-1) | Out-Null
    $netTask.Result
}

Function AwaitAction($WinRtAction) {
    $asTask = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and !$_.IsGenericMethod })[0]
    $netTask = $asTask.Invoke($null, @($WinRtAction))
    $netTask.Wait(-1) | Out-Null
}

[Windows.Storage.StorageFile,Windows.Storage,ContentType=WindowsRuntime] | Out-Null
		
try{
	$image = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($tempFile)) ([Windows.Storage.StorageFile])
    Write-Output "Image loaded from $tempFile"
}catch {
    Write-Output "Failed to load image from $tempFile"
    Write-Error $_ -ErrorAction SilentlyContinue
    Exit
} 
       
try{ 
    Write-Output "Setting image as lock screen image"
    AwaitAction ([Windows.System.UserProfile.LockScreen]::SetImageFileAsync($image))
    Write-Output "$tempFile configured as lock screen image"
    Remove-Item -Path $tempFile -Force -Confirm:$False
}catch{
    Write-Output "Failed to set lock screen image"
    Write-Error $_ -ErrorAction SilentlyContinue
} 

Write-Output "Script complete"
Stop-Transcript

Source: https://gitlab.com/Lieben/assortedFunctions/-/blob/master/set-windows10LockScreen.ps1

Deploying in user context:

Copying filesystem permissions for long paths using AlphaFS nad Powershell

AlphaFS is my go-to library when working with Long Paths, since PowerShell’s built in functions do not support long paths and error out.

As I couldn’t find good PowerShell examples using GetAccessControl and SetAccessControl with the AlphaFS library, I wanted to post my script here for those googling an example 🙂

Param(
    [String]$sourcePath,
    [String]$targetPath
)

[System.Reflection.Assembly]::UnsafeLoadFrom('https://gitlab.com/Lieben/assortedFunctions/-/raw/master/lib/AlphaFS.dll?inline=false')

$sourcePath = [Alphaleonis.Win32.Filesystem.Path]::GetLongPath($sourcePath)
$targetPath = [Alphaleonis.Win32.Filesystem.Path]::GetLongPath($targetPath)

if([Alphaleonis.Win32.Filesystem.Directory]::Exists($sourcePath)){
    Write-Verbose "Detected sourcePath as FOLDER"
    $sourceObject = New-Object Alphaleonis.Win32.Filesystem.DirectoryInfo($sourcePath)
}elseif([Alphaleonis.Win32.Filesystem.File]::Exists($sourcePath)){
    Write-Verbose "Detected sourcePath as FILE"
    $sourceObject = New-Object Alphaleonis.Win32.Filesystem.FileInfo($sourcePath)
}else{
    Throw "sourcePath not found"
}

if([Alphaleonis.Win32.Filesystem.Directory]::Exists($targetPath)){
    Write-Verbose "Detected targetPath as FOLDER"   
    $targetObject = New-Object Alphaleonis.Win32.Filesystem.DirectoryInfo($targetPath)
}elseif([Alphaleonis.Win32.Filesystem.File]::Exists($targetPath)){
    Write-Verbose "Detected targetPath as FILE"
    $targetObject = New-Object Alphaleonis.Win32.Filesystem.FileInfo($targetPath)
}else{
    Throw "targetPath not found"
}

$sourceACL = $sourceObject.GetAccessControl("Access")
$targetObject.SetAccessControl($sourceACL)

Git: https://gitlab.com/Lieben/assortedFunctions/-/blob/master/copy-longPathACL.ps1

Distributing Teams Backgrounds to all users using MEM

I’ve seen a few examples of distributing a set of teams backgrounds to users in MEM, mostly they seemed a little overly complex, especially in targetting all local users and/or packaging the script into an application. I wanted to:

  • Deploy using MEM PS script in user context
  • Avoid access controls on the storage location of the backgrounds
  • Simplify the creation of required Azure resources
  • Have a simple update procedure

Here’s my take, first autocreate a blob storage location using my template:

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/ARM%20templates/blob%20storage%20with%20container%20for%20Teams%20Backgrounds%20and%20public%20access.json

Note down the name you used for the storage account (which was the only parameter to the template), and use it to configure this PowerShell script:

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/add-teamsBackgroundsFromBlobContainer.ps1

Finally, deploy the script to a group of your users in MEM, in their own context:

Deploying a service principal to (CSP) child tenants

Cloud Solution Providers, or sometimes other types of Managed Service Providers often have to manage a large number of tenants. Ideally, they do their ‘Infrastructure As Code’.

Using various API’s to manage tenants is best done using a Service Principal instead of a user (MFA, lifecycle, etc).

Recently, I was tasked to provide a deployment method of a Service Principal (multi-tenant) to all child tenants of an MSP, including programmatically granting various Graph API permissions. The Graph endpoint for this (oauth2PermissionGrants) is still in Beta, but the other methods I wrote about in the past are not as reliable so we’re using the Beta endpoint.

The linked example script creates an SPN and grants AuditLog.Read.All. If you’re an MSP/CSP, you’ll probably want to capture the tenant ID’s you’re installing into, so you can easily administer these tenants centrally using your main multi-tenant SPN.

Moving forwards, you won’t need an admin user / service account in the tenants you manage anymore, at least for the API’s that support SPN’s.

https://gitlab.com/Lieben/assortedFunctions/-/blob/master/add-servicePrincipalToAllCSPChildTenants.ps1

Note: to completely remove module dependencies / login, check my independent token function.

Upserting Data to Azure SQL DB using PowerShell

For a project involving Azure Security I needed to store fairly large amounts of data in an Azure PaaS database using PowerShell.

If a row already exists, I want to do an UPDATE command, otherwise an INSERT command, also known as an UPSERT in SQL.

It should also use parameters to avoid issues with quotes in fields, and should convert PowerShell null’s/empty objects to the SQL equivalent.

The following function is what resulted, it only supports a single WHERE clause, but should be easy to enhance for those looking to improve 🙂

function invoke-sqlUpsert{
    Param(
        [String]$tableName,
        [Array]$values, # example: @(@{"column"="deviceId";"value"="123415";"dataType"=[Data.SQLDBType]::NVarChar})
        [PSObject]$primaryKey, # example: @{"column"="deviceId";"value"="123415";"dataType"=[Data.SQLDBType]::NVarChar}
        [System.Data.Common.DbConnection]$sqlConn
    )

    $sqlQuery = "BEGIN TRANSACTION;
    UPDATE $tableName
        SET "
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlQuery = "$sqlQuery$($values[$i].column)=@$($values[$i].column)"
        if($i -lt $values.Count-1){
            $sqlQuery = "$sqlQuery,"
        }
    }
    $sqlQuery = "$($sqlQuery)
        WHERE $($primaryKey.column) = @$($primaryKey.column);
    IF @@ROWCOUNT = 0
        BEGIN
            INSERT INTO $tableName ($($primaryKey.column),"
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlQuery = "$sqlQuery$($values[$i].column)"
        if($i -lt $values.Count-1){
            $sqlQuery = "$sqlQuery,"
        }
    }
    $sqlQuery = "$($sqlQuery)
            )
            VALUES (@$($primaryKey.column),"
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlQuery = "$($sqlQuery)@$($values[$i].column)"
        if($i -lt $values.Count-1){
            $sqlQuery = "$sqlQuery,"
        }
    }
    $sqlQuery = "$($sqlQuery));
        END
    COMMIT TRANSACTION;"
           

    $sqlCmd=new-object system.Data.SqlClient.SqlCommand($sqlQuery, $sqlConn)
    $sqlCmd.Parameters.Add((New-OBJECT DATA.SQLClient.SQLParameter("@$($primaryKey.column)",$primaryKey.dataType))) | OUT-NULL
    $sqlCmd.Parameters[0].Value = $primaryKey.value
    for($i = 0;$i -lt $values.Count;$i++){
        $sqlCmd.Parameters.Add((New-OBJECT DATA.SQLClient.SQLParameter("@$($values[$i].column)",$values[$i].dataType))) | OUT-NULL
        $sqlCmd.Parameters[$i+1].Value = $values[$i].value
    }

    for($i=0;$i -lt $sqlCmd.Parameters.count;$i++){
        if($sqlCmd.Parameters[$i].Value -eq $null){
            $sqlCmd.Parameters[$i].Value = [System.DBNull]::Value
        }
    }
    if($sqlCmd.ExecuteNonQuery() -ne 1){
        Throw $_
    }
}

An example of how to connect from an Azure Function before using this command:

using namespace System.Data.SqlClient
using namespace System.Net
$msi_authenticationResult = Invoke-RestMethod -Method Get -Headers @{'Secret' = $env:MSI_SECRET} -Uri ($env:MSI_ENDPOINT +'?resource=https://database.windows.net/&api-version=2017-09-01')
$sqlConn = New-Object System.Data.SqlClient.SqlConnection
$sqlConn.ConnectionString = "Data Source = yourserver.database.windows.net; Initial Catalog = yourdatabase"
$sqlConn.AccessToken = $msi_authenticationResult.access_token
$sqlConn.Open()

Microsoft 365, Azure, Automation & Code