Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Implement createWriteStream for Linux #15

Merged
merged 3 commits into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<vendor>Your Application Name</vendor>
<action id="my-application.pkexec.dd">
<description gettext-domain="my-application">Admin privileges required</description>
<message gettext-domain="my-application">Please enter your password to save this file</message>
<annotate key="org.freedesktop.policykit.exec.path">/bin/dd</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
```

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).
41 changes: 40 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -149,6 +149,45 @@ switch (process.platform) {
}
})
break

case 'linux':
Object.assign(fsAdmin, {
clearAuthorizationCache () {
spawnSync('/bin/pkcheck', ['--revoke-temp'])
},

createWriteStream (filePath) {
// Prompt for credentials synchronously to avoid creating multiple simultaneous prompts.
if (!fsAdmin.testMode && spawnSync('/usr/bin/pkexec', ['/bin/dd']).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) {
Expand Down
14 changes: 9 additions & 5 deletions test/fs-admin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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, '')
Expand Down Expand Up @@ -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')

Expand All @@ -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, '')

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down