Skip to content

Commit

Permalink
Merge pull request #7415 from jandubois/sudo-prompt
Browse files Browse the repository at this point in the history
Create "Rancher Desktop.app" for the sudo-prompt script on macOS
  • Loading branch information
mook-as authored Sep 10, 2024
2 parents 0f68745 + 1938702 commit 05623b8
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 147 deletions.
2 changes: 2 additions & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ APPDIR
appimage
appimagekit
APPLEID
applescript
APPLICATIONFOLDER
AProject
ARPNOMODIFY
Expand Down Expand Up @@ -568,6 +569,7 @@ opentelekomcloudcontainerengine
opsgenie
oracleoke
orsection
osacompile
osascript
osc
oswald
Expand Down
4 changes: 1 addition & 3 deletions pkg/rancher-desktop/backend/lima.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1381,9 +1381,7 @@ export default class LimaBackend extends events.EventEmitter implements VMBacken
*/
protected async sudoExec(this: unknown, command: string) {
await new Promise<void>((resolve, reject) => {
const iconPath = path.join(paths.resources, 'icons', 'logo-square-512.png');

sudo(command, { name: 'Rancher Desktop', icns: iconPath }, (error, stdout, stderr) => {
sudo(command, { name: 'Rancher Desktop' }, (error, stdout, stderr) => {
if (stdout) {
console.log(`Prompt for sudo: stdout: ${ stdout }`);
}
Expand Down
35 changes: 34 additions & 1 deletion pkg/rancher-desktop/sudo-prompt/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
# Rancher Desktop related changes

This module has been imported from https://github.com/jorangreef/sudo-prompt/tree/v9.2.1 (commit c3cc31a) and modified for Rancher Desktop:

The `applet.app` used to be included as a base64 encoded ZIP file inside `index.js` and extracted at runtime into a temp directory. The extracted app was renamed to match the `name` and `icns` specified by the caller, and the commands were written into `applet.app/Content/MacOS/sudo-prompt-command`.

The bundled applet did not include support for `aarch64` machines, so needed Rosetta2 installed to run. It was also not signed.

## Changes

The applet source code has been moved to `<repo>/src/sudo-prompt` and is built from source using `osacompile`, so `applet` will be an up-to-date universal binary supporting `x86_64` and `aarch64`.

The applet is placed into `<repo>/resources/darwin/internal/Rancher Desktop.app`. The app name is displayed as part of the dialog: "Rancher Desktop wants to make changes".

The `Contents/Info.plist` file has the `CFBundleName` set to "Rancher Desktop Password Prompt".

A `.icns` format icon has been created (the old `.png` file doesn't seem to work with the new applet) and is stored into `Contents/Resources/applet.icns`.

The `sudo-prompt-script` has been moved from `Contents/MacOS` to `Contents/Resources/Scripts` because it cannot be code-signed.

When the `RD_SUDO_PROMPT_OSASCRIPT` environment variable is set then the `Contents/Resources/Scripts/main.scpt` file (the compiled version of `sudo-prompt.applescript`) is executed via `osascript` instead of the applet. This will show an approval prompt that supports the Apple watch, or a touch id keyboard, but will not use the `Rancher Desktop` name or icon in the dialog.

The `sudo-prompt.applescript` has been modified to locate the `sudo-prompt-script` inside the applet because the working directory will no longer be inside the app.

All this means that the app can now be code-signed and notarized and will not be modified at runtime.

The app is being build by `yarn` during the `postinstall` phase with a custom dependency script.

The `index.js` code to modify the app at runtime has been removed and the logic simplified. `name` and `icns` options are ignored in the macOS `sudo` function.
<hr>

# Original CHANGELOG below

## [9.2.0] 2020-04-29

### Fixed
Expand Down Expand Up @@ -38,7 +71,7 @@
[#88](https://github.com/jorangreef/sudo-prompt/issues/88).

- Fix Windows to return `PERMISSION_DENIED` Error even when Windows' error
messages are internationalized, see
messages are internationalized, see
[#96](https://github.com/jorangreef/sudo-prompt/issues/96).

## [8.2.5] 2018-12-12
Expand Down
10 changes: 10 additions & 0 deletions pkg/rancher-desktop/sudo-prompt/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Rancher Desktop sudo-prompt

This module has been imported from [jorangreef/sudo-prompt: Run a command using sudo, prompting the user with an OS dialog if necessary.](https://github.com/jorangreef/sudo-prompt).

It is no longer a reusable module, but has been modified specifically for Rancher Desktop usage; see details in the [changelog](CHANGELOG.md).

<hr>

# Original README below

# sudo-prompt

Run a non-graphical terminal command using `sudo`, prompting the user with a graphical OS dialog if necessary. Useful for background Node.js applications or native Electron apps that need `sudo`.
Expand Down
185 changes: 47 additions & 138 deletions pkg/rancher-desktop/sudo-prompt/index.js

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions scripts/dependencies/sudo-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import path from 'path';

import { AlpineLimaISOVersion, Dependency, DownloadContext } from 'scripts/lib/dependencies';
import { simpleSpawn } from 'scripts/simple_process';

/**
* SudoPrompt represents the sudo-prompt.app applet used by sudo-prompt on macOS.
*/
export class SudoPrompt implements Dependency {
name = 'sudo-prompt';

async download(_: DownloadContext): Promise<void> {
// Rather than actually downloading anything, this builds the source code.
const sourceDir = path.join(process.cwd(), 'src', 'sudo-prompt');

console.log(`Building sudo-prompt applet`);
await simpleSpawn('./build-sudo-prompt', [], { cwd: sourceDir });
}

getAvailableVersions(_includePrerelease?: boolean | undefined): Promise<string[]> {
throw new Error('sudo-prompt dependencies do not have available versions.');
}

rcompareVersions(_version1: string | AlpineLimaISOVersion, _version2: string): 0 | 1 | -1 {
throw new Error('sudo-prompt dependencies do not have available versions.');
}
}
24 changes: 19 additions & 5 deletions scripts/lib/sign-macos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ async function *findFilesToSign(dir: string): AsyncIterable<string> {
continue; // We only sign regular files.
}

if (isBundleExecutable(fullPath)) {
if (await isBundleExecutable(fullPath)) {
// For bundles (apps and frameworks), we skip signing the executable
// itself as it will be signed when signing the bundle.
continue;
Expand Down Expand Up @@ -249,15 +249,29 @@ async function *findFilesToSign(dir: string): AsyncIterable<string> {
/**
* Detect if the path of a plain file indicates that it's the bundle executable
*/
function isBundleExecutable(fullPath: string): boolean {
async function isBundleExecutable(fullPath: string): Promise<boolean> {
const parts = fullPath.split(path.sep).reverse();

if (parts.length >= 4) {
// Foo.app/Contents/MacOS/Foo - the check style here avoids spell checker.
if (fullPath.endsWith(`${ parts[0] }.app/Contents/MacOS/${ parts[0] }`)) {
return true;
// Anything.app/Contents/MacOS/executable - the check style here avoids spell checker.
if (fullPath.endsWith(`.app/Contents/MacOS/${ parts[0] }`)) {
// Check Anything.app/Contents/Info.plist for CFBundleExecutable
const infoPlist = path.sep + path.join(...parts.slice(2).reverse(), 'Info.plist');

try {
const { stdout } = await spawnFile('/usr/bin/plutil',
['-extract', 'CFBundleExecutable', 'raw', '-expect', 'string', infoPlist],
{ stdio: 'pipe' });

return stdout.trimEnd() === parts[0];
} catch {
log.info({ infoPlist }, 'Failed to read Info.plist, assuming not the bundle executable.');

return false;
}
}
}

if (parts.length >= 4) {
// Foo.framework/Versions/A/Foo
if (parts[3] === `${ parts[0] }.framework` && parts[2] === 'Versions') {
Expand Down
13 changes: 13 additions & 0 deletions scripts/postinstall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import path from 'path';
import * as goUtils from 'scripts/dependencies/go-source';
import { Lima, LimaAndQemu, AlpineLimaISO } from 'scripts/dependencies/lima';
import { MobyOpenAPISpec } from 'scripts/dependencies/moby-openapi';
import { SudoPrompt } from 'scripts/dependencies/sudo-prompt';
import { ExtensionProxyImage, WSLDistroImage } from 'scripts/dependencies/tar-archives';
import * as tools from 'scripts/dependencies/tools';
import { Wix } from 'scripts/dependencies/wix';
Expand Down Expand Up @@ -46,6 +47,11 @@ const unixDependencies = [
new AlpineLimaISO(),
];

// Dependencies that are specific to macOS hosts.
const macOSDependencies = [
new SudoPrompt(),
];

// Dependencies that are specific to windows hosts.
const windowsDependencies = [
new WSLDistro(),
Expand Down Expand Up @@ -176,6 +182,13 @@ async function runScripts(): Promise<void> {
dependencies.push({ dependency, context: hostDownloadContext });
}

// download things for macOS host
if (platform === 'darwin') {
for (const dependency of macOSDependencies) {
dependencies.push({ dependency, context: hostDownloadContext });
}
}

// download things that go inside Lima VM
const vmDownloadContext = buildDownloadContextFor('linux', depVersions);

Expand Down
20 changes: 20 additions & 0 deletions src/sudo-prompt/build-sudo-prompt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash

# shellcheck disable=SC2164 # Use 'cd ... || exit' or 'cd ... || return' in case cd fails.
REPO=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.."; pwd)

# The APP name must be "Rancher Desktop.app" because this name is used in the dialog as
# "Rancher Desktop wants to make changes."
RESOURCES="${REPO}/resources"
APP="${RESOURCES}/darwin/internal/Rancher Desktop.app"
CONTENTS="${APP}/Contents"

rm -rf "$APP"
mkdir -p "$(dirname "$APP")"
osacompile -o "$APP" sudo-prompt.applescript

# Don't put the script into ${CONTENTS}/MacOS/ because that breaks signing the applet
cp sudo-prompt-script "${CONTENTS}/Resources/Scripts/"
sips -s format icns "${RESOURCES}/icons/mac-icon.png" --out "${CONTENTS}/Resources/applet.icns"

plutil -replace CFBundleName -string "Rancher Desktop Password Prompt" "${CONTENTS}/Info.plist"
19 changes: 19 additions & 0 deletions src/sudo-prompt/sudo-prompt-script
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
# This script is executed by the applet with root permissions.
# The caller will have created a temporary directory containing just the
# `sudo-prompt-command` shell script. This script will add the `code`,
# `stdout` and `stderr` files. The caller will delete this directory
# again after reading the files.

# Set sudo timestamp for subsequent sudo calls if tty_tickets are disabled:
/bin/mkdir -p /var/db/sudo/$USER > /dev/null 2>&1
/usr/bin/touch /var/db/sudo/$USER > /dev/null 2>&1
# AppleScript's "do shell script" may alter stdout line-endings.
# It may also set stdout to stderr if there was a non-zero return code and no stderr.
# We therefore prefer to redirect output streams and capture return code manually:
/bin/bash sudo-prompt-command 1>stdout 2>stderr
/bin/echo $? > code
# Correct ownership of stdout, stderr and code so that user can delete them:
/usr/sbin/chown $USER stdout stderr code
# Always return 0 so that AppleScript does not show error dialog:
exit 0
8 changes: 8 additions & 0 deletions src/sudo-prompt/sudo-prompt.applescript
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
set appletPath to POSIX path of (path to me)
if appletPath ends with ".app/" then
set appletPath to appletPath & "Contents/Resources/Scripts"
else
set appletPath to do shell script "dirname " & quoted form of appletPath
end if
set promptScript to appletPath & "/sudo-prompt-script"
do shell script (quoted form of promptScript) with administrator privileges

0 comments on commit 05623b8

Please sign in to comment.