mirror of
https://github.com/exelban/stats.git
synced 2026-05-07 20:02:34 +00:00
feat: added authorization step when trying to update Stats as non-admin (#3153)
This commit is contained in:
+63
-13
@@ -174,10 +174,7 @@ public class Updater {
|
||||
completion("DMG not found at \(dmg)")
|
||||
return
|
||||
}
|
||||
if !FileManager.default.isWritableFile(atPath: pwd) {
|
||||
completion("has no write permission on \(pwd)")
|
||||
return
|
||||
}
|
||||
let needsElevation = !FileManager.default.isWritableFile(atPath: pwd)
|
||||
|
||||
let diff = (Int(Date().timeIntervalSince1970) - self.lastInstallTS) / 60
|
||||
if diff <= 3 {
|
||||
@@ -242,16 +239,28 @@ public class Updater {
|
||||
|
||||
print("Script staged at \(scriptDst)")
|
||||
|
||||
let task = Process()
|
||||
task.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||
task.arguments = [scriptDst, "--app", pwd, "--dmg", dmg, "--mount", mountPoint]
|
||||
do {
|
||||
try task.run()
|
||||
} catch {
|
||||
completion("failed to launch updater: \(error)")
|
||||
return
|
||||
let scriptArgs = [scriptDst, "--app", pwd, "--dmg", dmg, "--mount", mountPoint, "--user", String(getuid())]
|
||||
|
||||
if needsElevation {
|
||||
if let err = self.runElevated("/bin/bash", args: scriptArgs) {
|
||||
_ = self.runProcess("/usr/bin/hdiutil", ["detach", mountPoint, "-force"])
|
||||
try? FileManager.default.removeItem(atPath: scriptDst)
|
||||
try? FileManager.default.removeItem(atPath: mountPoint)
|
||||
completion("elevated install failed: \(err)")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
let task = Process()
|
||||
task.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||
task.arguments = scriptArgs
|
||||
do {
|
||||
try task.run()
|
||||
} catch {
|
||||
completion("failed to launch updater: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
print("Run updater.sh with app: \(pwd) and dmg: \(dmg)")
|
||||
|
||||
self.lastInstallTS = Int(Date().timeIntervalSince1970)
|
||||
@@ -310,6 +319,47 @@ public class Updater {
|
||||
return dict[kSecCodeInfoTeamIdentifier as String] as? String
|
||||
}
|
||||
|
||||
private func runElevated(_ tool: String, args: [String]) -> String? {
|
||||
var authRef: AuthorizationRef?
|
||||
let createStatus = AuthorizationCreate(nil, nil, [], &authRef)
|
||||
guard createStatus == errAuthorizationSuccess, let authRef else {
|
||||
return "AuthorizationCreate failed (\(createStatus))"
|
||||
}
|
||||
defer { AuthorizationFree(authRef, [.destroyRights]) }
|
||||
|
||||
// AuthorizationExecuteWithPrivileges is deprecated since 10.7 but still functional;
|
||||
// resolve via dlsym to avoid the compile-time deprecation warning.
|
||||
typealias AEWPFn = @convention(c) (
|
||||
AuthorizationRef,
|
||||
UnsafePointer<CChar>,
|
||||
AuthorizationFlags,
|
||||
UnsafePointer<UnsafeMutablePointer<CChar>?>,
|
||||
UnsafeMutablePointer<UnsafeMutablePointer<FILE>?>?
|
||||
) -> OSStatus
|
||||
guard let sym = dlsym(UnsafeMutableRawPointer(bitPattern: -2), "AuthorizationExecuteWithPrivileges") else {
|
||||
return "AuthorizationExecuteWithPrivileges unavailable"
|
||||
}
|
||||
let aewp = unsafeBitCast(sym, to: AEWPFn.self)
|
||||
|
||||
var cArgs: [UnsafeMutablePointer<CChar>?] = args.map { strdup($0) }
|
||||
cArgs.append(nil)
|
||||
defer { cArgs.forEach { free($0) } }
|
||||
|
||||
let result: OSStatus = tool.withCString { toolPtr in
|
||||
cArgs.withUnsafeMutableBufferPointer { buf in
|
||||
aewp(authRef, toolPtr, [], buf.baseAddress!, nil)
|
||||
}
|
||||
}
|
||||
|
||||
if result == errAuthorizationCanceled {
|
||||
return "user canceled"
|
||||
}
|
||||
if result != errAuthorizationSuccess {
|
||||
return "AuthorizationExecuteWithPrivileges failed (\(result))"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func runProcess(_ launch: String, _ args: [String]) -> (output: String, error: String, exit: Int32) {
|
||||
let task = Process()
|
||||
task.executableURL = URL(fileURLWithPath: launch)
|
||||
|
||||
+24
-8
@@ -1,24 +1,40 @@
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
DMG_PATH="$HOME/Downloads/Stats.dmg"
|
||||
MOUNT_PATH="/tmp/Stats"
|
||||
APPLICATION_PATH="/Applications/"
|
||||
LAUNCH_UID=""
|
||||
|
||||
STEP=""
|
||||
|
||||
while [[ "$#" > 0 ]]; do case $1 in
|
||||
while [[ "$#" -gt 0 ]]; do case "$1" in
|
||||
-s|--step) STEP="$2"; shift;;
|
||||
-d|--dmg) DMG_PATH="$2"; shift;;
|
||||
-a|--app) APPLICATION_PATH="$2"; shift;;
|
||||
-m|--mount) MOUNT_PATH="$2"; shift;;
|
||||
-u|--user) LAUNCH_UID="$2"; shift;;
|
||||
*) echo "Unknown parameter passed: $1"; exit 1;;
|
||||
esac; shift; done
|
||||
|
||||
if [[ "$STEP" == "2" ]]; then
|
||||
rm -rf $APPLICATION_PATH/Stats.app
|
||||
cp -rf $MOUNT_PATH/Stats.app $APPLICATION_PATH/Stats.app
|
||||
APP_DST="${APPLICATION_PATH%/}/Stats.app"
|
||||
APP_SRC="${MOUNT_PATH%/}/Stats.app"
|
||||
|
||||
$APPLICATION_PATH/Stats.app/Contents/MacOS/Stats --dmg "$DMG_PATH"
|
||||
# When the script runs as root (admin auth path) but a target UID was passed,
|
||||
# launch the new app back as the original user so it doesn't run as root.
|
||||
launch_app() {
|
||||
if [[ -n "$LAUNCH_UID" && "$(id -u)" == "0" ]]; then
|
||||
/bin/launchctl asuser "$LAUNCH_UID" /usr/bin/sudo -u "#$LAUNCH_UID" "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$STEP" == "2" ]]; then
|
||||
rm -rf "$APP_DST"
|
||||
cp -rf "$APP_SRC" "$APP_DST"
|
||||
|
||||
launch_app "$APP_DST/Contents/MacOS/Stats" --dmg "$DMG_PATH"
|
||||
|
||||
echo "New version started"
|
||||
elif [[ "$STEP" == "3" ]]; then
|
||||
@@ -28,10 +44,10 @@ elif [[ "$STEP" == "3" ]]; then
|
||||
|
||||
echo "Done"
|
||||
else
|
||||
rm -rf $APPLICATION_PATH/Stats.app
|
||||
cp -rf $MOUNT_PATH/Stats.app $APPLICATION_PATH/Stats.app
|
||||
rm -rf "$APP_DST"
|
||||
cp -rf "$APP_SRC" "$APP_DST"
|
||||
|
||||
$APPLICATION_PATH/Stats.app/Contents/MacOS/Stats --dmg-path "$DMG_PATH" --mount-path "$MOUNT_PATH"
|
||||
launch_app "$APP_DST/Contents/MacOS/Stats" --dmg-path "$DMG_PATH" --mount-path "$MOUNT_PATH"
|
||||
|
||||
echo "New version started"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user