Files
autofirmer-expanded/scripts/enroll-chrome-management.ps1
Brandon LiandClaude Opus 5 8b2e78d766 Set Chrome as default and pin it, as far as Windows allows
Neither can be done for the running profile: UserChoice is hash-validated and
guarded by UCPD.sys since Feb 2024, and taskbar pinning lost its shell verb
years ago - both to stop installers helping themselves. What is supported is
policy, which applies when a profile is created, so this writes the
DefaultAssociationsConfiguration and taskbar layout XMLs for the next user and
does the two things that help the current one: a desktop shortcut, and opening
the Settings page where the browser choice actually has to be made.

Folded into the enrolment script's existing elevation so the install still
raises a single UAC prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 18:17:42 -05:00

209 lines
9.3 KiB
PowerShell

<#
.SYNOPSIS
Enrol this machine's Chrome in Chrome Enterprise Core.
.DESCRIPTION
Chrome refuses to force-install an extension from outside its own web store
unless the machine counts as enterprise managed. Enrolment is the way to
qualify without joining a domain: one machine-level token, and from the next
launch Chrome takes policy from the Google Admin console and the off-store
restriction lifts. install-extension-policy.ps1 then works as intended.
The token comes from the Admin console: Devices > Chrome > Managed browsers >
the enrollment-token button. One token can enrol any number of machines, and
it is only used at enrolment.
Needs an elevated PowerShell — the token is machine-scoped, and Chrome does
not read it from the user hive.
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\enroll-chrome-management.ps1 -Token abcd1234-...
#>
[CmdletBinding()]
param(
# Defaulted so setup is hands-off. An enrolment token is reusable and enrols
# any number of browsers, which is what makes a baked-in default work at all.
# It is a modest secret: anyone holding it can enrol *their* browser into the
# org, which gets them our policies and a row in the console — it grants no
# access to the Google account or to anything else. Rotate it in Devices >
# Chrome > Managed browsers and change it here.
[Parameter(ParameterSetName = 'Enrol')]
[string]$Token = 'ce4fa6cd-e940-45f6-9086-85075d6cff34',
# Refuse to start Chrome at all if enrolment fails. Sound on a fleet, risky
# on a trading PC: a revoked token would leave you with no browser.
[Parameter(ParameterSetName = 'Enrol')]
[switch]$Mandatory,
[Parameter(Mandatory = $true, ParameterSetName = 'Remove')]
[switch]$Remove,
# Fail instead of asking for elevation. Also the loop-breaker: the elevated
# copy is launched with this set, so it can never spawn another.
[switch]$NoElevate,
# Fail rather than installing Chrome when it is missing.
[switch]$SkipChromeInstall,
# Skip the default-browser and taskbar-pin policies.
[switch]$NoDesktopConfig
)
$ErrorActionPreference = 'Stop'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
if (-not ([Security.Principal.WindowsPrincipal]$identity).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
if ($NoElevate) {
throw 'Run this from an elevated PowerShell — the enrolment token is machine-level.'
}
# Relaunch elevated rather than making the caller do it. setup-windows.bat
# runs unelevated on purpose — nothing else it does needs admin — so this is
# the one UAC prompt in the whole install, and it is raised here where the
# arguments are already parsed and can be passed on intact.
Write-Host 'Asking for administrator rights — the enrolment token is machine-level.'
$psArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$PSCommandPath`"", '-NoElevate')
if ($SkipChromeInstall) { $psArgs += '-SkipChromeInstall' }
if ($NoDesktopConfig) { $psArgs += '-NoDesktopConfig' }
if ($Remove) { $psArgs += '-Remove' }
else {
$psArgs += @('-Token', $Token)
if ($Mandatory) { $psArgs += '-Mandatory' }
}
$elevated = Start-Process -FilePath (Get-Process -Id $PID).Path -Verb RunAs -Wait -PassThru `
-ArgumentList $psArgs
exit $elevated.ExitCode
}
# ── Chrome itself ────────────────────────────────────────────────────────────
# Enrolling a browser that is not installed would write a token nothing ever
# reads. Chromium is deliberately not accepted as a substitute: it has no
# cloud-management support, so it can never be enrolled and would stay subject
# to the off-store restriction forever.
function Find-Chrome {
foreach ($exe in @("$env:ProgramFiles\Google\Chrome\Application\chrome.exe",
"${env:ProgramFiles(x86)}\Google\Chrome\Application\chrome.exe",
"$env:LOCALAPPDATA\Google\Chrome\Application\chrome.exe")) {
if ($exe -and (Test-Path $exe)) { return $exe }
}
return $null
}
if (-not $Remove -and -not (Find-Chrome)) {
if ($SkipChromeInstall) { throw 'Google Chrome is not installed, and -SkipChromeInstall was given.' }
# Google's own enterprise MSI, which installs machine-wide and silently. We
# are already elevated by this point, so this needs no second prompt.
$url = 'https://dl.google.com/dl/chrome/install/googlechromestandaloneenterprise64.msi'
$msi = Join-Path $env:TEMP 'googlechromestandaloneenterprise64.msi'
Write-Host 'Google Chrome is not installed — downloading it from Google (about 160 MB).'
# Three ways down, fastest first. Invoke-WebRequest is last for a reason: it
# pushes every buffer through the PowerShell pipeline, and on Windows
# PowerShell it also re-renders the progress bar each time, which on a file
# this size costs more than the transfer itself — minutes rather than
# seconds, and indistinguishable from a stall while you watch it crawl.
$curl = Join-Path $env:SystemRoot 'System32\curl.exe'
if (Test-Path $curl) {
# -C - resumes a part-downloaded file rather than starting the 160 MB
# again, which matters when this is retried after an interruption.
& $curl -fL --retry 3 --retry-delay 2 -C - -o $msi $url
if ($LASTEXITCODE -ne 0) { Remove-Item $msi -ErrorAction SilentlyContinue }
}
if (-not (Test-Path $msi) -and (Get-Command Start-BitsTransfer -ErrorAction SilentlyContinue)) {
try {
Start-BitsTransfer -Source $url -Destination $msi -Priority Foreground -ErrorAction Stop
} catch {
Write-Host "BITS transfer failed ($($_.Exception.Message)); falling back."
}
}
if (-not (Test-Path $msi)) {
$previousProgress = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing
} finally {
$ProgressPreference = $previousProgress
}
}
# A truncated download installs as a corrupt package, with an msiexec error
# that says nothing about the real cause.
$expected = 0
try {
$expected = [int64](Invoke-WebRequest -Uri $url -Method Head -UseBasicParsing).Headers['Content-Length']
} catch { }
$got = (Get-Item $msi -ErrorAction SilentlyContinue).Length
if (-not $got) { throw 'Chrome download produced no file.' }
if ($expected -gt 0 -and $got -ne $expected) {
Remove-Item $msi -ErrorAction SilentlyContinue
throw "Chrome download is incomplete ($got of $expected bytes). Re-run to resume."
}
Write-Host 'Installing Chrome...'
$msiexec = Start-Process msiexec.exe -Wait -PassThru -ArgumentList @('/i', "`"$msi`"", '/qn', '/norestart')
Remove-Item $msi -ErrorAction SilentlyContinue
if ($msiexec.ExitCode -ne 0) { throw "Chrome install failed (msiexec exit $($msiexec.ExitCode))." }
if (-not (Find-Chrome)) { throw 'Chrome still not found after installing.' }
Write-Host 'Chrome installed.'
}
$key = 'HKLM:\SOFTWARE\Policies\Google\Chrome'
if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null }
if ($Remove) {
foreach ($name in 'CloudManagementEnrollmentToken', 'CloudManagementEnrollmentMandatory') {
if ($null -ne (Get-ItemProperty -Path $key -Name $name -ErrorAction SilentlyContinue)) {
Remove-ItemProperty -Path $key -Name $name
Write-Host "Removed $name"
}
}
Write-Host @"
Restart Chrome. Note this stops Chrome asking for policy, but does not undo the
enrolment on Google's side — delete the browser from Devices > Chrome > Managed
browsers as well, or it keeps a seat and shows as stale.
"@
exit 0
}
New-ItemProperty -Path $key -Name 'CloudManagementEnrollmentToken' `
-Value $Token -PropertyType String -Force | Out-Null
Write-Host "Wrote CloudManagementEnrollmentToken to $key"
if ($Mandatory) {
New-ItemProperty -Path $key -Name 'CloudManagementEnrollmentMandatory' `
-Value 1 -PropertyType DWord -Force | Out-Null
Write-Host 'Enrolment is now mandatory — Chrome will not start unenrolled.'
}
# Already elevated here, so folding this in keeps the whole install to one UAC
# prompt rather than two.
if (-not $NoDesktopConfig) {
$defaults = Join-Path (Split-Path -Parent $PSCommandPath) 'configure-chrome-defaults.ps1'
if (Test-Path $defaults) {
try {
& $defaults -Quiet
} catch {
Write-Warning "Default-browser and taskbar policies skipped: $($_.Exception.Message)"
}
}
}
Write-Host @"
Next:
1. Restart Chrome completely (check Task Manager for stray chrome.exe).
2. chrome://policy should name the Admin console as a policy source, and
chrome://management should say the browser is managed.
3. The machine appears under Devices > Chrome > Managed browsers.
4. Then install the extension, which is no longer blocked:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 -Browser Chrome
Or skip that and force-install it from the Admin console instead, which
covers every enrolled machine at once.
"@