diff --git a/README.md b/README.md index 26873ea..76d51f7 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,45 @@ Perform file system operations with administrator privileges. npm install fs-admin ``` +## Packaging (Linux only) + +This library uses [PolicyKit](https://wiki.archlinux.org/index.php/Polkit) to escalate privileges when calling `createWriteStream(path)` on Linux. In particular, it will invoke `pkexec dd of=path` to stream the desired bytes into the specified location. + +### PolicyKit + +Not all Linux distros may include PolicyKit as part of their standard installation. As such, it is recommended to make it an explicit dependency of your application package. The following is an example Debian control file that requires `policykit-1` to be installed as part of `my-application`: + +``` +Package: my-application +Version: 1.0.0 +Depends: policykit-1 +``` + +### Policies + +When using this library as part of a Linux application, you may want to install a [Policy](https://wiki.archlinux.org/index.php/PolicyKit#Actions) as well. Although not mandatory, policy files allow customizing the behavior of `pkexec` by e.g., displaying a custom password prompt or retaining admin privileges for a short period of time: + +```xml + + + + Your Application Name + + Admin privileges required + Please enter your password to save this file + /bin/dd + true + + auth_admin_keep + auth_admin_keep + auth_admin_keep + + + +``` + +Policy files should be installed in `/usr/share/polkit-1/actions` as part of your application's installation script. + +For more information, you can find a complete example of requiring PolicyKit and distributing policy files in the [Atom repository](https://github.com/atom/atom/pull/19412). diff --git a/index.js b/index.js index adde11b..3a9def5 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ const fs = require('fs') -const { spawn } = require('child_process') +const { spawn, spawnSync } = require('child_process') const EventEmitter = require('events') const binding = require('./build/Release/fs_admin.node') const fsAdmin = module.exports @@ -149,6 +149,50 @@ switch (process.platform) { } }) break + + case 'linux': + Object.assign(fsAdmin, { + clearAuthorizationCache () { + spawnSync('/bin/pkcheck', ['--revoke-temp']) + }, + + createWriteStream (filePath) { + if (!fsAdmin.testMode) { + // Prompt for credentials synchronously to avoid creating multiple simultaneous prompts. + const noopCommand = spawnSync('/usr/bin/pkexec', ['/bin/dd']) + if (noopCommand.error || noopCommand.status !== 0) { + const result = new EventEmitter() + result.write = result.end = function () {} + process.nextTick(() => { + result.emit('error', new Error('Failed to obtain credentials')) + }) + return result + } + } + + const dd = fsAdmin.testMode + ? spawn('/bin/dd', ['of=' + filePath]) + : spawn('/usr/bin/pkexec', ['/bin/dd', 'of=' + filePath]) + + const stream = new EventEmitter() + stream.write = (chunk, encoding, callback) => { + dd.stdin.write(chunk, encoding, callback) + } + stream.end = (callback) => { + if (callback) stream.on('finish', callback) + dd.stdin.end() + } + dd.on('exit', (exitCode) => { + if (exitCode !== 0) { + stream.emit('error', new Error('dd exited with code ' + exitCode)) + } + stream.emit('finish') + }) + + return stream + } + }) + break } function wrapCallback (commandName, callback) { diff --git a/test/fs-admin.test.js b/test/fs-admin.test.js index dd04745..31104a8 100644 --- a/test/fs-admin.test.js +++ b/test/fs-admin.test.js @@ -7,10 +7,6 @@ const fsAdmin = require('..') // Comment this out to test with actual privilege escalation. fsAdmin.testMode = true -if (process.platform !== 'win32' && process.platform !== 'darwin') { - process.exit(0) -} - describe('fs-admin', function () { let dirPath, filePath @@ -23,7 +19,7 @@ describe('fs-admin', function () { if (!fsAdmin.testMode) this.timeout(10000) describe('createWriteStream', () => { - if (process.platform !== 'darwin') return + if (process.platform === 'win32') return it('writes to the given file as the admin user', (done) => { fs.writeFileSync(filePath, '') @@ -74,6 +70,8 @@ describe('fs-admin', function () { }) describe('makeTree', () => { + if (process.platform === 'linux') return + it('creates a directory at the given path as the admin user', (done) => { const pathToCreate = path.join(dirPath, 'dir1', 'dir2', 'dir3') @@ -92,6 +90,8 @@ describe('fs-admin', function () { }) describe('unlink', () => { + if (process.platform === 'linux') return + it('deletes the given file as the admin user', (done) => { fs.writeFileSync(filePath, '') @@ -126,6 +126,8 @@ describe('fs-admin', function () { }) describe('symlink', () => { + if (process.platform === 'linux') return + it('creates a symlink at the given path as the admin user', (done) => { fsAdmin.symlink(__filename, filePath, (error) => { assert.strictEqual(error, null) @@ -141,6 +143,8 @@ describe('fs-admin', function () { }) describe('recursiveCopy', () => { + if (process.platform === 'linux') return + it('copies the given folder to the given location as the admin user', (done) => { const sourcePath = path.join(dirPath, 'src-dir') fs.mkdirSync(sourcePath)