Implement user access permissions and update deeplink handling in Windows installer
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

- Added a new macro in `grant-users-access.nsh` to grant standard users read and execute access to the installation directory, enhancing accessibility for non-admin users.
- Updated `installer.nsh` to include the new macro during installation, ensuring proper permissions are set.
- Changed the output file from `deeplink.js` to `deeplink.exe` in the build process, improving the executable handling for the application.
- Refactored the installer script to ensure the correct command is used for the farmcontrol URI handler, enhancing functionality.
- Introduced a new script, `patch-windows-binaries.mjs`, to set the requested execution level for Windows binaries, improving security and compliance.
This commit is contained in:
Tom Butcher 2026-08-03 00:59:07 +01:00
parent 7d1dc22327
commit f641ea390d
8 changed files with 75 additions and 14 deletions

View File

@ -73,6 +73,8 @@ Section "Farm Control" SecMain
"NoRepair" 1
WriteUninstaller "$INSTDIR\Uninstall.exe"
!insertmacro grantUsersInstallAccess
SectionEnd
Section "Uninstall"

View File

@ -1,12 +1,31 @@
; Grant standard (non-admin) users read/execute on the install tree.
; Elevated installs into Program Files often create admin-only ACLs, which
; breaks launcher.exe and bun reading deeplink.js for normal users.
; breaks launcher.exe and deeplink handling for normal users.
!macro grantUsersInstallAccess
Push $0
Push $1
DetailPrint "Granting standard users read and execute access to $INSTDIR"
ExecWait '"$SYSDIR\icacls.exe" "$INSTDIR" /inheritance:e /T /C' $0
DetailPrint "Enabled inherited permissions (exit code: $0)"
ExecWait '"$SYSDIR\icacls.exe" "$INSTDIR" /grant *S-1-5-32-545:(OI)(CI)RX /T /C' $0
StrCpy $1 "$INSTDIR"
InitPluginsDir
FileOpen $0 "$PLUGINSDIR\grant-access.ps1" w
FileWrite $0 '$$path = "'
FileWrite $0 $1
FileWrite $0 '"$\r$\n'
FileWrite $0 '$$icacls = Join-Path $$env:SystemRoot "System32\icacls.exe"$\r$\n'
FileWrite $0 'function Invoke-Icacls {$\r$\n'
FileWrite $0 ' param([string[]]$$Arguments)$\r$\n'
FileWrite $0 ' $$proc = Start-Process -FilePath $$icacls -ArgumentList $$Arguments -Wait -PassThru -NoNewWindow$\r$\n'
FileWrite $0 ' Write-Host ("icacls " + ($$Arguments -join " ") + " -> " + $$proc.ExitCode)$\r$\n'
FileWrite $0 '}$\r$\n'
FileWrite $0 'Invoke-Icacls @($$path, "/reset", "/T", "/C")$\r$\n'
FileWrite $0 'Invoke-Icacls @($$path, "/inheritance:e", "/T", "/C")$\r$\n'
FileWrite $0 'Invoke-Icacls @($$path, "/grant", "BUILTIN\Users:(OI)(CI)RX", "/T", "/C")$\r$\n'
FileWrite $0 'Invoke-Icacls @($$path, "/grant", "NT AUTHORITY\Authenticated Users:(OI)(CI)RX", "/T", "/C")$\r$\n'
FileWrite $0 'Invoke-Icacls @($$path, "/grant", "ALL APPLICATION PACKAGES:(OI)(CI)RX", "/T", "/C")$\r$\n'
FileClose $0
ExecWait '"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File "$PLUGINSDIR\grant-access.ps1"' $0
DetailPrint "Granted Users read/execute (exit code: $0)"
Delete "$PLUGINSDIR\grant-access.ps1"
Pop $1
Pop $0
!macroend

View File

@ -78,8 +78,6 @@
!include "grant-users-access.nsh"
!macro customInstall
!insertmacro grantUsersInstallAccess
DetailPrint "Register farmcontrol URI Handler"
DeleteRegKey HKCR "farmcontrol"
WriteRegStr HKCR "farmcontrol" "" "URL:farmcontrol"
@ -87,7 +85,7 @@
WriteRegStr HKCR "farmcontrol\DefaultIcon" "" "$INSTDIR\bin\launcher.exe"
WriteRegStr HKCR "farmcontrol\shell" "" ""
WriteRegStr HKCR "farmcontrol\shell\Open" "" ""
WriteRegStr HKCR "farmcontrol\shell\Open\command" "" '"$INSTDIR\bin\bun.exe" "$INSTDIR\bin\deeplink.js" "%1"'
WriteRegStr HKCR "farmcontrol\shell\Open\command" "" '"$INSTDIR\bin\deeplink.exe" "%1"'
DetailPrint "Creating shortcuts"
!insertmacro createDesktopShortcut

View File

@ -11,15 +11,16 @@ export function stageWindowsDeeplink(appDir) {
throw new Error(`stage-windows-deeplink: entrypoint not found: ${entrypoint}`)
}
const outputPath = path.join(appDir, 'bin', 'deeplink.js')
const outputPath = path.join(appDir, 'bin', 'deeplink.exe')
mkdirSync(path.dirname(outputPath), { recursive: true })
const result = spawnSync(
'bun',
[
'build',
'--compile',
'--minify',
'--target=bun',
'--target=bun-windows-x64',
entrypoint,
'--outfile',
outputPath

View File

@ -71,12 +71,12 @@ if ($stagedAppBytes -lt 5242880) {
}
$requiredExe = Join-Path $stagingAppDir "bin\launcher.exe"
$requiredDeeplinkScript = Join-Path $stagingAppDir "bin\deeplink.js"
$requiredDeeplinkExe = Join-Path $stagingAppDir "bin\deeplink.exe"
if (-not (Test-Path $requiredExe)) {
throw "Staged application is missing bin\launcher.exe"
}
if (-not (Test-Path $requiredDeeplinkScript)) {
throw "Staged application is missing bin\deeplink.js"
if (-not (Test-Path $requiredDeeplinkExe)) {
throw "Staged application is missing bin\deeplink.exe"
}
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol.nsi")

View File

@ -20,6 +20,7 @@ import {
expandWindowsAppFromArchive
} from './expand-windows-installer.mjs'
import { stageWindowsDeeplink } from './build-windows-deeplink.mjs'
import { patchWindowsBinaries } from './patch-windows-binaries.mjs'
import { codesignMacAppBundle } from './codesign-macos-app.mjs'
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
@ -662,6 +663,7 @@ async function main() {
platformDir
)
stageWindowsDeeplinkScript(appDir)
await patchWindowsBinaries(appDir)
let published
try {

View File

@ -0,0 +1,39 @@
import { existsSync, readdirSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { rcedit } from 'rcedit'
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
export async function patchWindowsBinaries(appDir) {
if (process.platform !== 'win32') {
return
}
const binDir = path.join(appDir, 'bin')
if (!existsSync(binDir)) {
throw new Error(`patch-windows-binaries: bin directory not found at ${binDir}`)
}
const executables = readdirSync(binDir).filter((name) =>
name.toLowerCase().endsWith('.exe')
)
for (const executable of executables) {
const executablePath = path.join(binDir, executable)
await rcedit(executablePath, {
'requested-execution-level': 'asInvoker'
})
console.log(`patch-windows-binaries: set asInvoker on ${executablePath}`)
}
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const appDirArg = process.argv[2]
if (!appDirArg) {
console.error('Usage: bun scripts/patch-windows-binaries.mjs <app-dir>')
process.exit(1)
}
await patchWindowsBinaries(path.resolve(appDirArg))
}

View File

@ -1,4 +1,4 @@
import { existsSync } from 'node:fs'
import { chdirSync, existsSync } from 'node:fs'
import { dirname, join } from 'node:path'
const NATIVE_WRAPPER_DLL = 'libNativeWrapper.dll'
@ -34,7 +34,7 @@ export function ensureWindowsWorkingDirectory() {
if (!wrapperInCwd && wrapperInBinDir) {
try {
existsSync(binDir)
chdirSync(binDir)
} catch {
// Ignore if we cannot change directory.
}