<# .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 ) $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 ($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.' } 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. "@