mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +00:00
refactor: extract elevation routines to lib/shared/permissions.js (#1351)
The elevation mechanism currently embedded in `lib/child-writer/writer-proxy.js` is extracted as a separate re-usable function in `lib/shared/permissions.js`. This change hugely simplifies the writer proxy, while allowing us to iterate faster on the elevation core functionality. Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
parent
0696833ad6
commit
62ca0e5b09
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -18,15 +18,12 @@
|
||||
|
||||
const Bluebird = require('bluebird');
|
||||
const childProcess = require('child_process');
|
||||
const commandJoin = require('command-join');
|
||||
const ipc = require('node-ipc');
|
||||
const _ = require('lodash');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'));
|
||||
const utils = require('./utils');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
const errors = require('../shared/errors');
|
||||
const robot = require('../shared/robot');
|
||||
const permissions = require('../shared/permissions');
|
||||
const packageJSON = require('../../package.json');
|
||||
@ -72,61 +69,8 @@ return permissions.isElevated().then((elevated) => {
|
||||
if (!elevated) {
|
||||
console.log('Attempting to elevate');
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
const elevator = Bluebird.promisifyAll(require('elevator'));
|
||||
|
||||
const commandArguments = [
|
||||
'set',
|
||||
'ELECTRON_RUN_AS_NODE=1',
|
||||
'&&',
|
||||
'set',
|
||||
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
|
||||
'&&',
|
||||
'set',
|
||||
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`,
|
||||
'&&',
|
||||
|
||||
// This is a trick to make the binary afterwards catch
|
||||
// the environment variables set just previously.
|
||||
'call'
|
||||
|
||||
].concat(process.argv);
|
||||
|
||||
// For debugging purposes
|
||||
console.log(`Running: ${commandArguments.join(' ')}`);
|
||||
|
||||
return elevator.executeAsync(commandArguments, {
|
||||
hidden: true,
|
||||
terminating: true,
|
||||
doNotPushdCurrentDirectory: true,
|
||||
waitForTermination: true
|
||||
}).catch({
|
||||
code: 'ELEVATE_CANCELLED'
|
||||
}, () => {
|
||||
process.exit(EXIT_CODES.CANCELLED);
|
||||
});
|
||||
}
|
||||
|
||||
const commandArguments = _.attempt(() => {
|
||||
const commandPrefix = [
|
||||
|
||||
// Some elevation tools, like `pkexec` or `kdesudo`, don't
|
||||
// provide a way to preserve the environment, therefore we
|
||||
// have to make sure the environment variables we're interested
|
||||
// in are manually inherited.
|
||||
'env',
|
||||
'ELECTRON_RUN_AS_NODE=1',
|
||||
`IPC_SERVER_ID=${process.env.IPC_SERVER_ID}`,
|
||||
`IPC_CLIENT_ID=${process.env.IPC_CLIENT_ID}`,
|
||||
|
||||
// This environment variable prevents the AppImages
|
||||
// desktop integration script from presenting the
|
||||
// "installation" dialog.
|
||||
'SKIP=1'
|
||||
|
||||
];
|
||||
|
||||
if (process.env.APPIMAGE && process.env.APPDIR) {
|
||||
if (os.platform() === 'linux' && process.env.APPIMAGE && process.env.APPDIR) {
|
||||
|
||||
// Translate the current arguments to point to the AppImage
|
||||
// Relative paths are resolved from `/tmp/.mount_XXXXXX/usr`
|
||||
@ -135,44 +79,32 @@ return permissions.isElevated().then((elevated) => {
|
||||
.invokeMap('replace', path.join(process.env.APPDIR, 'usr/'), '')
|
||||
.value();
|
||||
|
||||
return commandPrefix
|
||||
.concat([ process.env.APPIMAGE ])
|
||||
.concat(translatedArguments);
|
||||
return _.concat([ process.env.APPIMAGE ], translatedArguments);
|
||||
}
|
||||
|
||||
return commandPrefix.concat(process.argv);
|
||||
return process.argv;
|
||||
});
|
||||
|
||||
const command = commandJoin(commandArguments);
|
||||
|
||||
// For debugging purposes
|
||||
console.log(`Running: ${command}`);
|
||||
console.log(`Running: ${commandArguments.join(' ')}`);
|
||||
|
||||
return permissions.elevateCommand(commandArguments, {
|
||||
applicationName: packageJSON.displayName,
|
||||
environment: {
|
||||
ELECTRON_RUN_AS_NODE: 1,
|
||||
IPC_SERVER_ID: process.env.IPC_SERVER_ID,
|
||||
IPC_CLIENT_ID: process.env.IPC_CLIENT_ID,
|
||||
|
||||
// This environment variable prevents the AppImages
|
||||
// desktop integration script from presenting the
|
||||
// "installation" dialog.
|
||||
SKIP: 1
|
||||
|
||||
return sudoPrompt.execAsync(command, {
|
||||
name: packageJSON.displayName
|
||||
}).then((stdout, stderr) => {
|
||||
if (!_.isEmpty(stderr)) {
|
||||
throw errors.createError({
|
||||
title: stderr
|
||||
});
|
||||
}
|
||||
|
||||
// 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`.
|
||||
|
||||
}).catch({
|
||||
message: 'User did not grant permission.'
|
||||
}, () => {
|
||||
process.exit(EXIT_CODES.CANCELLED);
|
||||
}).catch({
|
||||
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'
|
||||
});
|
||||
}).then((results) => {
|
||||
if (results.cancelled) {
|
||||
process.exit(EXIT_CODES.CANCELLED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,7 +19,10 @@
|
||||
const os = require('os');
|
||||
const Bluebird = require('bluebird');
|
||||
const childProcess = Bluebird.promisifyAll(require('child_process'));
|
||||
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'));
|
||||
const commandJoin = require('command-join');
|
||||
const _ = require('lodash');
|
||||
const errors = require('./errors');
|
||||
|
||||
/**
|
||||
* @summary The user id of the UNIX "superuser"
|
||||
@ -52,7 +55,7 @@ const UNIX_SUPERUSER_USER_ID = 0;
|
||||
* });
|
||||
*/
|
||||
exports.isElevated = () => {
|
||||
if (process.platform === 'win32') {
|
||||
if (os.platform() === 'win32') {
|
||||
|
||||
// `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10
|
||||
// Works even when the "Server" service is disabled
|
||||
@ -67,3 +70,132 @@ exports.isElevated = () => {
|
||||
|
||||
return Bluebird.resolve(process.geteuid() === UNIX_SUPERUSER_USER_ID);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get environment command prefix
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Object} environment - environment map
|
||||
* @returns {String[]} command arguments
|
||||
*
|
||||
* @example
|
||||
* const commandPrefix = permissions.getEnvironmentCommandPrefix({
|
||||
* FOO: 'bar',
|
||||
* BAR: 'baz'
|
||||
* });
|
||||
*
|
||||
* childProcess.execSync(_.join(_.concat(commandPrefix, [ 'mycommand' ]), ' '));
|
||||
*/
|
||||
exports.getEnvironmentCommandPrefix = (environment) => {
|
||||
const isWindows = os.platform() === 'win32';
|
||||
|
||||
if (_.isEmpty(environment)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const argv = _.flatMap(environment, (value, key) => {
|
||||
if (_.isNil(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
return [ 'set', `${key}=${value}`, '&&' ];
|
||||
}
|
||||
|
||||
return [ `${key}=${value}` ];
|
||||
});
|
||||
|
||||
if (isWindows) {
|
||||
|
||||
// This is a trick to make the binary afterwards catch
|
||||
// the environment variables set just previously.
|
||||
return _.concat(argv, [ 'call' ]);
|
||||
|
||||
}
|
||||
|
||||
return _.concat([ 'env' ], argv);
|
||||
};
|
||||
|
||||
/**
|
||||
* @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 = (command, options) => {
|
||||
const prefixedCommand = _.concat(exports.getEnvironmentCommandPrefix(options.environment), command);
|
||||
|
||||
if (os.platform() === 'win32') {
|
||||
const elevator = Bluebird.promisifyAll(require('elevator'));
|
||||
|
||||
return elevator.executeAsync(prefixedCommand, {
|
||||
hidden: true,
|
||||
terminating: true,
|
||||
doNotPushdCurrentDirectory: true,
|
||||
waitForTermination: true
|
||||
}).then(() => {
|
||||
return {
|
||||
cancelled: false
|
||||
};
|
||||
}).catch({
|
||||
code: 'ELEVATE_CANCELLED'
|
||||
}, () => {
|
||||
return {
|
||||
cancelled: true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return sudoPrompt.execAsync(commandJoin(prefixedCommand), {
|
||||
name: options.applicationName
|
||||
}).then((stdout, stderr) => {
|
||||
if (!_.isEmpty(stderr)) {
|
||||
throw errors.createError({
|
||||
title: stderr
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
cancelled: false
|
||||
};
|
||||
|
||||
// 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`.
|
||||
|
||||
}).catch({
|
||||
message: 'User did not grant permission.'
|
||||
}, () => {
|
||||
return {
|
||||
cancelled: true
|
||||
};
|
||||
}).catch({
|
||||
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'
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
246
tests/shared/permissions.spec.js
Normal file
246
tests/shared/permissions.spec.js
Normal file
@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2017 resin.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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const m = require('mochainon');
|
||||
const os = require('os');
|
||||
const permissions = require('../../lib/shared/permissions');
|
||||
|
||||
describe('Shared: permissions', function() {
|
||||
|
||||
describe('.getEnvironmentCommandPrefix()', function() {
|
||||
|
||||
describe('given windows', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.osPlatformStub = m.sinon.stub(os, 'platform');
|
||||
this.osPlatformStub.returns('win32');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
this.osPlatformStub.restore();
|
||||
});
|
||||
|
||||
it('should return an empty array if no environment', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix()).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should return an empty array environment is an empty object', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({})).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should create an environment command prefix out of one variable', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 'bar'
|
||||
})).to.deep.equal([
|
||||
'set',
|
||||
'FOO=bar',
|
||||
'&&',
|
||||
'call'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create an environment command prefix out of many variables', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 'bar',
|
||||
BAR: 'baz',
|
||||
BAZ: 'qux'
|
||||
})).to.deep.equal([
|
||||
'set',
|
||||
'FOO=bar',
|
||||
'&&',
|
||||
'set',
|
||||
'BAR=baz',
|
||||
'&&',
|
||||
'set',
|
||||
'BAZ=qux',
|
||||
'&&',
|
||||
'call'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore undefined and null variable values', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: null,
|
||||
BAR: undefined,
|
||||
BAZ: 'qux'
|
||||
})).to.deep.equal([
|
||||
'set',
|
||||
'BAZ=qux',
|
||||
'&&',
|
||||
'call'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stringify number values', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 1,
|
||||
BAR: 0,
|
||||
BAZ: -1
|
||||
})).to.deep.equal([
|
||||
'set',
|
||||
'FOO=1',
|
||||
'&&',
|
||||
'set',
|
||||
'BAR=0',
|
||||
'&&',
|
||||
'set',
|
||||
'BAZ=-1',
|
||||
'&&',
|
||||
'call'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('given linux', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.osPlatformStub = m.sinon.stub(os, 'platform');
|
||||
this.osPlatformStub.returns('linux');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
this.osPlatformStub.restore();
|
||||
});
|
||||
|
||||
it('should return an empty array if no environment', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix()).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should return an empty array environment is an empty object', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({})).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should create an environment command prefix out of one variable', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 'bar'
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'FOO=bar'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create an environment command prefix out of many variables', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 'bar',
|
||||
BAR: 'baz',
|
||||
BAZ: 'qux'
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'FOO=bar',
|
||||
'BAR=baz',
|
||||
'BAZ=qux'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore undefined and null variable values', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: null,
|
||||
BAR: undefined,
|
||||
BAZ: 'qux'
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'BAZ=qux'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stringify number values', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 1,
|
||||
BAR: 0,
|
||||
BAZ: -1
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'FOO=1',
|
||||
'BAR=0',
|
||||
'BAZ=-1'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('given darwin', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.osPlatformStub = m.sinon.stub(os, 'platform');
|
||||
this.osPlatformStub.returns('darwin');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
this.osPlatformStub.restore();
|
||||
});
|
||||
|
||||
it('should return an empty array if no environment', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix()).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should return an empty array environment is an empty object', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({})).to.deep.equal([]);
|
||||
});
|
||||
|
||||
it('should create an environment command prefix out of one variable', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 'bar'
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'FOO=bar'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should create an environment command prefix out of many variables', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 'bar',
|
||||
BAR: 'baz',
|
||||
BAZ: 'qux'
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'FOO=bar',
|
||||
'BAR=baz',
|
||||
'BAZ=qux'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should ignore undefined and null variable values', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: null,
|
||||
BAR: undefined,
|
||||
BAZ: 'qux'
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'BAZ=qux'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stringify number values', function() {
|
||||
m.chai.expect(permissions.getEnvironmentCommandPrefix({
|
||||
FOO: 1,
|
||||
BAR: 0,
|
||||
BAZ: -1
|
||||
})).to.deep.equal([
|
||||
'env',
|
||||
'FOO=1',
|
||||
'BAR=0',
|
||||
'BAZ=-1'
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user