mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-25 07:47:18 +00:00
Convert permissions.js to typescript
Change-type: patch
This commit is contained in:
parent
b5593ef5b2
commit
efe953d8cd
@ -23,6 +23,7 @@ import { Badge, Checkbox, Modal } from 'rendition';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { version } from '../../../../../package.json';
|
||||
import { Dictionary } from '../../../../shared/utils';
|
||||
import * as settings from '../../models/settings';
|
||||
import * as store from '../../models/store';
|
||||
import * as analytics from '../../modules/analytics';
|
||||
@ -118,10 +119,6 @@ interface SettingsModalProps {
|
||||
toggleModal: (value: boolean) => void;
|
||||
}
|
||||
|
||||
interface Dictionary<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
export const SettingsModal: any = styled(
|
||||
({ toggleModal }: SettingsModalProps) => {
|
||||
const [currentSettings, setCurrentSettings]: [
|
||||
|
@ -27,6 +27,7 @@ const settings = require('../models/settings')
|
||||
const flashState = require('../models/flash-state')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../shared/errors')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const permissions = require('../../../shared/permissions')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const windowProgress = require('../os/window-progress')
|
||||
|
@ -198,7 +198,7 @@ export function createError(options: {
|
||||
export function createUserError(options: {
|
||||
title: string;
|
||||
description: string;
|
||||
code: string;
|
||||
code?: string;
|
||||
}): Error {
|
||||
return createError({
|
||||
title: options.title,
|
||||
|
@ -1,241 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method,quotes,no-magic-numbers,require-jsdoc */
|
||||
|
||||
'use strict'
|
||||
|
||||
const bindings = require('bindings')
|
||||
const Bluebird = require('bluebird')
|
||||
const childProcess = Bluebird.promisifyAll(require('child_process'))
|
||||
const fs = require('fs')
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
const semver = require('semver')
|
||||
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'))
|
||||
const { promisify } = require('util')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('./errors')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { tmpFileDisposer } = require('./utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { sudo: catalinaSudo } = require('./catalina-sudo/sudo')
|
||||
|
||||
const writeFileAsync = promisify(fs.writeFile)
|
||||
|
||||
/**
|
||||
* @summary The user id of the UNIX "superuser"
|
||||
* @constant
|
||||
* @type {Number}
|
||||
*/
|
||||
const UNIX_SUPERUSER_USER_ID = 0
|
||||
|
||||
/**
|
||||
* @summary Check if the current process is running with elevated permissions
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This function has been adapted from https://github.com/sindresorhus/is-elevated,
|
||||
* which was originally licensed under MIT.
|
||||
*
|
||||
* We're not using such module directly given that it
|
||||
* contains dependencies with dynamic undeclared dependencies,
|
||||
* causing a mess when trying to concatenate the code.
|
||||
*
|
||||
* @fulfil {Boolean} - whether the current process has elevated permissions
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* permissions.isElevated().then((isElevated) => {
|
||||
* if (isElevated) {
|
||||
* console.log('This process has elevated permissions');
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
exports.isElevated = () => {
|
||||
if (os.platform() === 'win32') {
|
||||
// `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10
|
||||
// Works even when the "Server" service is disabled
|
||||
// See http://stackoverflow.com/a/28268802
|
||||
return childProcess.execAsync('fltmc')
|
||||
.then(_.constant(true))
|
||||
.catch({
|
||||
code: os.constants.errno.EPERM
|
||||
}, _.constant(false))
|
||||
}
|
||||
|
||||
return Bluebird.resolve(process.geteuid() === UNIX_SUPERUSER_USER_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if the current process is running with elevated permissions
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* permissions.isElevatedUnixSync()
|
||||
* if (isElevated) {
|
||||
* console.log('This process has elevated permissions');
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
exports.isElevatedUnixSync = () => {
|
||||
return (process.geteuid() === UNIX_SUPERUSER_USER_ID)
|
||||
}
|
||||
|
||||
const escapeSh = (value) => {
|
||||
// Make sure it's a string
|
||||
// Replace ' -> '\'' (closing quote, escaped quote, opening quote)
|
||||
// Surround with quotes
|
||||
return `'${String(value).replace(/'/g, "'\\''")}'`
|
||||
}
|
||||
|
||||
const escapeParamCmd = (value) => {
|
||||
// Make sure it's a string
|
||||
// Escape " -> \"
|
||||
// Surround with double quotes
|
||||
return `"${String(value).replace(/"/g, '\\"')}"`
|
||||
}
|
||||
|
||||
const setEnvVarSh = (value, name) => {
|
||||
return `export ${name}=${escapeSh(value)}`
|
||||
}
|
||||
|
||||
const setEnvVarCmd = (value, name) => {
|
||||
return `set "${name}=${String(value)}"`
|
||||
}
|
||||
|
||||
// Exported for tests
|
||||
exports.createLaunchScript = (command, argv, environment) => {
|
||||
const isWindows = os.platform() === 'win32'
|
||||
const lines = []
|
||||
if (isWindows) {
|
||||
// Switch to utf8
|
||||
lines.push('chcp 65001')
|
||||
}
|
||||
const [ setEnvVarFn, escapeFn ] = isWindows ? [ setEnvVarCmd, escapeParamCmd ] : [ setEnvVarSh, escapeSh ]
|
||||
lines.push(..._.map(environment, setEnvVarFn))
|
||||
lines.push([ command, ...argv ].map(escapeFn).join(' '))
|
||||
return lines.join(os.EOL)
|
||||
}
|
||||
|
||||
const elevateScriptWindows = async (path) => {
|
||||
// 'elevator' imported here as it only exists on windows
|
||||
// TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed
|
||||
const elevateAsync = promisify(bindings({ bindings: 'elevator' }).elevate)
|
||||
|
||||
// '&' needs to be escaped here (but not when written to a .cmd file)
|
||||
const cmd = [ 'cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&') ]
|
||||
const { cancelled } = await elevateAsync(cmd)
|
||||
return { cancelled }
|
||||
}
|
||||
|
||||
const elevateScriptUnix = async (path, name) => {
|
||||
const cmd = [ 'bash', escapeSh(path) ].join(' ')
|
||||
const [ , stderr ] = await sudoPrompt.execAsync(cmd, { name })
|
||||
if (!_.isEmpty(stderr)) {
|
||||
throw errors.createError({ title: stderr })
|
||||
}
|
||||
return { cancelled: false }
|
||||
}
|
||||
|
||||
const elevateScriptCatalina = async (path) => {
|
||||
const cmd = [ 'bash', escapeSh(path) ].join(' ')
|
||||
try {
|
||||
const { cancelled } = await catalinaSudo(cmd)
|
||||
return { cancelled }
|
||||
} catch (error) {
|
||||
return errors.createError({ title: error.stderr })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Elevate a command
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String[]} command - command arguments
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.applicationName - application name
|
||||
* @param {Object} options.environment - environment variables
|
||||
* @fulfil {Object} - elevation results
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* permissions.elevateCommand([ 'foo', 'bar' ], {
|
||||
* applicationName: 'My App',
|
||||
* environment: {
|
||||
* FOO: 'bar'
|
||||
* }
|
||||
* }).then((results) => {
|
||||
* if (results.cancelled) {
|
||||
* console.log('Elevation has been cancelled');
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
exports.elevateCommand = async (command, options) => {
|
||||
if (await exports.isElevated()) {
|
||||
await childProcess.execFileAsync(command[0], command.slice(1), { env: options.environment })
|
||||
return { cancelled: false }
|
||||
}
|
||||
const isWindows = os.platform() === 'win32'
|
||||
const launchScript = exports.createLaunchScript(command[0], command.slice(1), options.environment)
|
||||
return Bluebird.using(tmpFileDisposer({ postfix: '.cmd' }), async ({ path }) => {
|
||||
await writeFileAsync(path, launchScript)
|
||||
if (isWindows) {
|
||||
return elevateScriptWindows(path)
|
||||
}
|
||||
if (os.platform() === 'darwin' && semver.compare(os.release(), '19.0.0') >= 0) {
|
||||
// >= macOS Catalina
|
||||
return elevateScriptCatalina(path)
|
||||
}
|
||||
try {
|
||||
return await elevateScriptUnix(path, options.applicationName)
|
||||
} catch (error) {
|
||||
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
||||
// There doesn't seem to be a better way to handle these errors, so
|
||||
// for now, we should make sure we double check if the error messages
|
||||
// have changed every time we upgrade `sudo-prompt`.
|
||||
console.log('error', error)
|
||||
if (_.includes(error.message, 'is not in the sudoers file')) {
|
||||
throw errors.createUserError({
|
||||
title: "Your user doesn't have enough privileges to proceed",
|
||||
description: 'This application requires sudo privileges to be able to write to drives'
|
||||
})
|
||||
} else if (_.startsWith(error.message, 'Command failed:')) {
|
||||
throw errors.createUserError({
|
||||
title: 'The elevated process died unexpectedly',
|
||||
description: `The process error code was ${error.code}`
|
||||
})
|
||||
} else if (error.message === 'User did not grant permission.') {
|
||||
return { cancelled: true }
|
||||
} else if (error.message === 'No polkit authentication agent found.') {
|
||||
throw errors.createUserError({
|
||||
title: 'No polkit authentication agent found',
|
||||
description: 'Please install a polkit authentication agent for your desktop environment of choice to continue'
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
206
lib/shared/permissions.ts
Executable file
206
lib/shared/permissions.ts
Executable file
@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2017 balena.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import bindings = require('bindings');
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as childProcess from 'child_process';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as semver from 'semver';
|
||||
import * as sudoPrompt from 'sudo-prompt';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { sudo as catalinaSudo } from './catalina-sudo/sudo';
|
||||
import * as errors from './errors';
|
||||
import { Dictionary, tmpFileDisposer } from './utils';
|
||||
|
||||
const execAsync = promisify(childProcess.exec);
|
||||
const execFileAsync = promisify(childProcess.execFile);
|
||||
const sudoExecAsync = promisify(sudoPrompt.exec);
|
||||
|
||||
/**
|
||||
* @summary The user id of the UNIX "superuser"
|
||||
*/
|
||||
const UNIX_SUPERUSER_USER_ID = 0;
|
||||
|
||||
export async function isElevated(): Promise<boolean> {
|
||||
if (os.platform() === 'win32') {
|
||||
// `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10
|
||||
// Works even when the "Server" service is disabled
|
||||
// See http://stackoverflow.com/a/28268802
|
||||
try {
|
||||
await execAsync('fltmc');
|
||||
} catch (error) {
|
||||
if (error.code === os.constants.errno.EPERM) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return process.geteuid() === UNIX_SUPERUSER_USER_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if the current process is running with elevated permissions
|
||||
*/
|
||||
export function isElevatedUnixSync(): boolean {
|
||||
return process.geteuid() === UNIX_SUPERUSER_USER_ID;
|
||||
}
|
||||
|
||||
function escapeSh(value: any): string {
|
||||
// Make sure it's a string
|
||||
// Replace ' -> '\'' (closing quote, escaped quote, opening quote)
|
||||
// Surround with quotes
|
||||
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function escapeParamCmd(value: any): string {
|
||||
// Make sure it's a string
|
||||
// Escape " -> \"
|
||||
// Surround with double quotes
|
||||
return `"${String(value).replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
|
||||
function setEnvVarSh(value: any, name: string): string {
|
||||
return `export ${name}=${escapeSh(value)}`;
|
||||
}
|
||||
|
||||
function setEnvVarCmd(value: any, name: string): string {
|
||||
return `set "${name}=${String(value)}"`;
|
||||
}
|
||||
|
||||
// Exported for tests
|
||||
export function createLaunchScript(
|
||||
command: string,
|
||||
argv: string[],
|
||||
environment: Dictionary<string>,
|
||||
): string {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const lines = [];
|
||||
if (isWindows) {
|
||||
// Switch to utf8
|
||||
lines.push('chcp 65001');
|
||||
}
|
||||
const [setEnvVarFn, escapeFn] = isWindows
|
||||
? [setEnvVarCmd, escapeParamCmd]
|
||||
: [setEnvVarSh, escapeSh];
|
||||
lines.push(..._.map(environment, setEnvVarFn));
|
||||
lines.push([command, ...argv].map(escapeFn).join(' '));
|
||||
return lines.join(os.EOL);
|
||||
}
|
||||
|
||||
async function elevateScriptWindows(
|
||||
path: string,
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
// 'elevator' imported here as it only exists on windows
|
||||
// TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed
|
||||
const elevateAsync = promisify(bindings('elevator').elevate);
|
||||
|
||||
// '&' needs to be escaped here (but not when written to a .cmd file)
|
||||
const cmd = ['cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&')];
|
||||
const { cancelled } = await elevateAsync(cmd);
|
||||
return { cancelled };
|
||||
}
|
||||
|
||||
async function elevateScriptUnix(
|
||||
path: string,
|
||||
name: string,
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
const cmd = ['bash', escapeSh(path)].join(' ');
|
||||
const [, stderr] = await sudoExecAsync(cmd, { name });
|
||||
if (!_.isEmpty(stderr)) {
|
||||
throw errors.createError({ title: stderr });
|
||||
}
|
||||
return { cancelled: false };
|
||||
}
|
||||
|
||||
async function elevateScriptCatalina(
|
||||
path: string,
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
const cmd = ['bash', escapeSh(path)].join(' ');
|
||||
try {
|
||||
const { cancelled } = await catalinaSudo(cmd);
|
||||
return { cancelled };
|
||||
} catch (error) {
|
||||
throw errors.createError({ title: error.stderr });
|
||||
}
|
||||
}
|
||||
|
||||
export async function elevateCommand(
|
||||
command: string[],
|
||||
options: { environment: Dictionary<string>; applicationName: string },
|
||||
): Promise<{ cancelled: boolean }> {
|
||||
if (await exports.isElevated()) {
|
||||
await execFileAsync(command[0], command.slice(1), {
|
||||
env: options.environment,
|
||||
});
|
||||
return { cancelled: false };
|
||||
}
|
||||
const isWindows = os.platform() === 'win32';
|
||||
const launchScript = exports.createLaunchScript(
|
||||
command[0],
|
||||
command.slice(1),
|
||||
options.environment,
|
||||
);
|
||||
return Bluebird.using(
|
||||
tmpFileDisposer({ postfix: '.cmd' }),
|
||||
async ({ path }) => {
|
||||
await fs.writeFile(path, launchScript);
|
||||
if (isWindows) {
|
||||
return elevateScriptWindows(path);
|
||||
}
|
||||
if (
|
||||
os.platform() === 'darwin' &&
|
||||
semver.compare(os.release(), '19.0.0') >= 0
|
||||
) {
|
||||
// >= macOS Catalina
|
||||
return elevateScriptCatalina(path);
|
||||
}
|
||||
try {
|
||||
return await elevateScriptUnix(path, options.applicationName);
|
||||
} catch (error) {
|
||||
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
||||
// There doesn't seem to be a better way to handle these errors, so
|
||||
// for now, we should make sure we double check if the error messages
|
||||
// have changed every time we upgrade `sudo-prompt`.
|
||||
console.log('error', error);
|
||||
if (_.includes(error.message, 'is not in the sudoers file')) {
|
||||
throw errors.createUserError({
|
||||
title: "Your user doesn't have enough privileges to proceed",
|
||||
description:
|
||||
'This application requires sudo privileges to be able to write to drives',
|
||||
});
|
||||
} else if (_.startsWith(error.message, 'Command failed:')) {
|
||||
throw errors.createUserError({
|
||||
title: 'The elevated process died unexpectedly',
|
||||
description: `The process error code was ${error.code}`,
|
||||
});
|
||||
} else if (error.message === 'User did not grant permission.') {
|
||||
return { cancelled: true };
|
||||
} else if (error.message === 'No polkit authentication agent found.') {
|
||||
throw errors.createUserError({
|
||||
title: 'No polkit authentication agent found',
|
||||
description:
|
||||
'Please install a polkit authentication agent for your desktop environment of choice to continue',
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
@ -24,6 +24,10 @@ import * as errors from './errors';
|
||||
|
||||
const getAsync = promisify(request.get);
|
||||
|
||||
export interface Dictionary<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
export function isValidPercentage(percentage: any): boolean {
|
||||
return _.every([_.isNumber(percentage), percentage >= 0, percentage <= 100]);
|
||||
}
|
||||
|
12
npm-shrinkwrap.json
generated
12
npm-shrinkwrap.json
generated
@ -1092,6 +1092,12 @@
|
||||
"defer-to-connect": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@types/bindings": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/bindings/-/bindings-1.3.0.tgz",
|
||||
"integrity": "sha512-mTWOE6wC64MoEpv33otJNpQob81l5Pi+NsUkdiiP8EkESraQM94zuus/2s/Vz2Idy1qQkctNINYDZ61nfG1ngQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/bluebird": {
|
||||
"version": "3.5.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.28.tgz",
|
||||
@ -1317,6 +1323,12 @@
|
||||
"@types/htmlparser2": "*"
|
||||
}
|
||||
},
|
||||
"@types/semver": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.0.tgz",
|
||||
"integrity": "sha512-1OzrNb4RuAzIT7wHSsgZRlMBlNsJl+do6UblR7JMW4oB7bbR+uBEYtUh7gEc/jM84GGilh68lSOokyM/zNUlBA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/styled-components": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-4.1.8.tgz",
|
||||
|
@ -94,10 +94,12 @@
|
||||
"@babel/plugin-proposal-function-bind": "^7.2.0",
|
||||
"@babel/preset-env": "^7.6.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@types/bindings": "^1.3.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/node": "^12.12.24",
|
||||
"@types/react-dom": "^16.8.4",
|
||||
"@types/request": "^2.48.4",
|
||||
"@types/semver": "^6.2.0",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"chalk": "^1.1.3",
|
||||
|
@ -20,6 +20,7 @@
|
||||
|
||||
const m = require('mochainon')
|
||||
const os = require('os')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const permissions = require('../../lib/shared/permissions')
|
||||
|
||||
describe('Shared: permissions', function () {
|
||||
|
1
typings/sudo-prompt/index.d.ts
vendored
Normal file
1
typings/sudo-prompt/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'sudo-prompt';
|
Loading…
x
Reference in New Issue
Block a user