diff --git a/deploy/README.md b/deploy/README.md index 63f333d..49189fc 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -93,12 +93,16 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-po sudo scripts/install-extension-policy.sh ``` +Both configure every Chromium-family browser they find — Chrome, Edge and Brave +take the same CRX, the same update manifest and the same policy, differing only +in where that policy is written. Narrow it with `-Browser Edge` if you would +rather not. + The Windows script writes to the current user's policy hive and needs no elevation; `-Scope Machine` covers every account on the box and does need an elevated shell. Linux has no per-user equivalent — Chrome only reads managed policy from `/etc` — so that one is root-only and machine-wide. Both take -`-Remove` / `--remove`, and the Windows one takes `-Browser Edge` or -`-Browser Brave`. +`-Remove` / `--remove`. Afterwards, remove the unpacked copy from `chrome://extensions` — it now has the same id, and Chrome will not load both — and restart the browser. diff --git a/scripts/install-extension-policy.ps1 b/scripts/install-extension-policy.ps1 index b81922d..6137c69 100644 --- a/scripts/install-extension-policy.ps1 +++ b/scripts/install-extension-policy.ps1 @@ -3,12 +3,16 @@ Install the AutoFirmer extension from our own update server, via policy. .DESCRIPTION - Chrome will not side-load an extension on Windows — dropping a CRX on - chrome://extensions is refused — and it will not update an unpacked one. The - supported way to run an extension you host yourself is a policy: + Chrome will not side-load a CRX on Windows — dropping one on + chrome://extensions is refused — and it will not update an unpacked folder. + The supported way to run an extension you host yourself is a policy: ExtensionInstallForcelist names the extension id and the update manifest, and - from then on Chrome installs it, keeps it updated on its own schedule, and - re-installs it if it goes missing. + from then on the browser installs it, keeps it updated on its own schedule, + and re-installs it if it goes missing. + + Edge and Brave are Chromium too: same CRX, same policy, same update + manifest, only a different registry vendor key. By default this configures + every one of the three that is actually installed. Defaults to the current user's policy hive, which needs no elevation. -Scope Machine writes to HKLM instead, for every account on the box; that @@ -16,8 +20,8 @@ After this runs: * Remove the unpacked copy from chrome://extensions. It has the same id - now, and Chrome will not load both. - * Restart Chrome, or open chrome://policy and press "Reload policies". + now, and the browser will not load both. + * Restart the browser, or open chrome://policy and press "Reload policies". * A force-installed extension cannot be disabled or removed from the extensions page. Re-run with -Remove to take it off. @@ -25,12 +29,12 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 .EXAMPLE - powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 -Scope Machine -Browser Edge + powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-extension-policy.ps1 -Browser Edge -Scope Machine #> [CmdletBinding()] param( - [ValidateSet('Chrome', 'Edge', 'Brave')] - [string]$Browser = 'Chrome', + [ValidateSet('All', 'Chrome', 'Edge', 'Brave')] + [string]$Browser = 'All', [ValidateSet('User', 'Machine')] [string]$Scope = 'User', @@ -45,11 +49,28 @@ param( $ErrorActionPreference = 'Stop' +# Vendor key under SOFTWARE\Policies, and the executable each one registers. $vendors = @{ Chrome = 'Google\Chrome' Edge = 'Microsoft\Edge' Brave = 'BraveSoftware\Brave' } +$exes = @{ + Chrome = 'chrome.exe' + Edge = 'msedge.exe' + Brave = 'brave.exe' +} + +function Test-BrowserInstalled([string]$Name) { + # App Paths is where every one of these registers itself, per-machine or + # per-user depending on how it was installed. + foreach ($hive in @('HKLM:', 'HKCU:')) { + if (Test-Path "$hive\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\$($exes[$Name])") { + return $true + } + } + return $false +} $hive = if ($Scope -eq 'Machine') { 'HKLM:' } else { 'HKCU:' } @@ -61,55 +82,76 @@ if ($Scope -eq 'Machine') { } } -$key = "$hive\SOFTWARE\Policies\$($vendors[$Browser])\ExtensionInstallForcelist" +$targets = if ($Browser -eq 'All') { + @('Chrome', 'Edge', 'Brave') | Where-Object { Test-BrowserInstalled $_ } +} else { + @($Browser) +} + +if (-not $targets) { + throw 'Found no Chrome, Edge or Brave installation. Name one with -Browser if it is installed somewhere unusual.' +} + $entry = "$Id;$UpdateUrl" -if ($Remove) { - if (-not (Test-Path $key)) { Write-Host "Nothing to remove — no $Scope forcelist for $Browser."; exit 0 } - $props = Get-ItemProperty $key - $found = $false - foreach ($name in ($props.PSObject.Properties.Name | Where-Object { $_ -notlike 'PS*' })) { - if ($props.$name -like "$Id;*") { - Remove-ItemProperty -Path $key -Name $name - Write-Host "Removed $name ($($props.$name))" - $found = $true +foreach ($target in $targets) { + $key = "$hive\SOFTWARE\Policies\$($vendors[$target])\ExtensionInstallForcelist" + + if ($Remove) { + if (-not (Test-Path $key)) { + Write-Host "$target : nothing to remove — no $Scope forcelist." + continue } + $props = Get-ItemProperty $key + $found = $false + foreach ($name in ($props.PSObject.Properties.Name | Where-Object { $_ -notlike 'PS*' })) { + if ($props.$name -like "$Id;*") { + Remove-ItemProperty -Path $key -Name $name + Write-Host "$target : removed entry $name ($($props.$name))" + $found = $true + } + } + if (-not $found) { Write-Host "$target : nothing to remove — $Id was not in the $Scope forcelist." } + continue } - if (-not $found) { Write-Host "Nothing to remove — $Id was not in the $Browser $Scope forcelist." } - Write-Host "`nRestart $Browser to let the change take effect." + + if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null } + + # The forcelist is a list expressed as numbered values, so adding to it means + # finding a free number rather than overwriting whatever is already there — + # another extension may well be in the list. + $props = Get-ItemProperty $key + $existing = $props.PSObject.Properties | + Where-Object { $_.Name -notlike 'PS*' -and $_.Value -like "$Id;*" } + + if ($existing) { + Set-ItemProperty -Path $key -Name $existing.Name -Value $entry + Write-Host "$target : updated $Scope forcelist entry $($existing.Name)" + } else { + $used = $props.PSObject.Properties.Name | + Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ } + $slot = if ($used) { (($used | Measure-Object -Maximum).Maximum + 1) } else { 1 } + New-ItemProperty -Path $key -Name "$slot" -Value $entry -PropertyType String -Force | Out-Null + Write-Host "$target : added $Scope forcelist entry $slot" + } + Write-Host " $key" +} + +if ($Remove) { + Write-Host "`nRestart the browser to let the change take effect." exit 0 } -if (-not (Test-Path $key)) { New-Item -Path $key -Force | Out-Null } - -# The forcelist is a list expressed as numbered values, so adding to it means -# finding a free number rather than overwriting whatever is already there — -# another extension may well be in the list. -$props = Get-ItemProperty $key -$existing = $props.PSObject.Properties | - Where-Object { $_.Name -notlike 'PS*' -and $_.Value -like "$Id;*" } - -if ($existing) { - Set-ItemProperty -Path $key -Name $existing.Name -Value $entry - Write-Host "Updated the $Browser $Scope forcelist entry $($existing.Name):" -} else { - $used = $props.PSObject.Properties.Name | - Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ } - $slot = if ($used) { (($used | Measure-Object -Maximum).Maximum + 1) } else { 1 } - New-ItemProperty -Path $key -Name "$slot" -Value $entry -PropertyType String -Force | Out-Null - Write-Host "Added $Browser $Scope forcelist entry ${slot}:" -} - -Write-Host " $key" -Write-Host " $entry" Write-Host @" + $entry + Next: 1. Remove the unpacked copy from chrome://extensions (same id — both cannot load). - 2. Restart $Browser, or open chrome://policy and press "Reload policies". - 3. Check chrome://extensions: the card should say "Installed by enterprise policy". + 2. Restart the browser, or open its chrome://policy page and press "Reload policies". + 3. The card should read "Installed by enterprise policy". -From here $Browser updates it on its own. The extension also asks for a check as -soon as the dashboard reports a newer version, so a release normally lands in -seconds rather than waiting out Chrome's own cycle. +From here the browser updates it on its own. The extension also asks for a check +as soon as the dashboard reports a newer version, so a release normally lands in +seconds rather than waiting out the browser's own cycle. "@ diff --git a/setup-windows.bat b/setup-windows.bat index 2c9477b..8652375 100644 --- a/setup-windows.bat +++ b/setup-windows.bat @@ -331,9 +331,10 @@ REM the extension is installed the way a self-hosted one has to be: a policy REM entry naming our update server. Written to this user's hive, which needs no REM elevation - install-extension-policy.ps1 -Scope Machine covers every account. echo. -echo [7/7] Chrome extension -echo Installs it from the update server and lets Chrome keep it current -echo by itself. No administrator rights needed. +echo [7/7] Browser extension +echo Installs it from the update server into whichever of Chrome, Edge +echo and Brave are present, and lets them keep it current by themselves. +echo No administrator rights needed. set "DOEXT=" set /p "DOEXT= Set this up now? [Y/n] " if /i "%DOEXT%"=="n" goto :ext_skipped