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

Commit 3076674

Browse files
Antonio Scandurrashiftkey
authored andcommitted
Implement createWriteStream for Linux (#15)
* Implement `createWriteStream` for Linux * Add packaging information * Restructure synchronous prompt command invocation
1 parent 86dab87 commit 3076674

File tree

3 files changed

+96
-6
lines changed

3 files changed

+96
-6
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,45 @@ Perform file system operations with administrator privileges.
1111
npm install fs-admin
1212
```
1313

14+
## Packaging (Linux only)
15+
16+
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.
17+
18+
### PolicyKit
19+
20+
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`:
21+
22+
```
23+
Package: my-application
24+
Version: 1.0.0
25+
Depends: policykit-1
26+
```
27+
28+
### Policies
29+
30+
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:
31+
32+
```xml
33+
<?xml version="1.0" encoding="UTF-8"?>
34+
<!DOCTYPE policyconfig PUBLIC
35+
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
36+
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
37+
<policyconfig>
38+
<vendor>Your Application Name</vendor>
39+
<action id="my-application.pkexec.dd">
40+
<description gettext-domain="my-application">Admin privileges required</description>
41+
<message gettext-domain="my-application">Please enter your password to save this file</message>
42+
<annotate key="org.freedesktop.policykit.exec.path">/bin/dd</annotate>
43+
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
44+
<defaults>
45+
<allow_any>auth_admin_keep</allow_any>
46+
<allow_inactive>auth_admin_keep</allow_inactive>
47+
<allow_active>auth_admin_keep</allow_active>
48+
</defaults>
49+
</action>
50+
</policyconfig>
51+
```
52+
53+
Policy files should be installed in `/usr/share/polkit-1/actions` as part of your application's installation script.
54+
55+
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).

index.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require('fs')
2-
const { spawn } = require('child_process')
2+
const { spawn, spawnSync } = require('child_process')
33
const EventEmitter = require('events')
44
const binding = require('./build/Release/fs_admin.node')
55
const fsAdmin = module.exports
@@ -149,6 +149,50 @@ switch (process.platform) {
149149
}
150150
})
151151
break
152+
153+
case 'linux':
154+
Object.assign(fsAdmin, {
155+
clearAuthorizationCache () {
156+
spawnSync('/bin/pkcheck', ['--revoke-temp'])
157+
},
158+
159+
createWriteStream (filePath) {
160+
if (!fsAdmin.testMode) {
161+
// Prompt for credentials synchronously to avoid creating multiple simultaneous prompts.
162+
const noopCommand = spawnSync('/usr/bin/pkexec', ['/bin/dd'])
163+
if (noopCommand.error || noopCommand.status !== 0) {
164+
const result = new EventEmitter()
165+
result.write = result.end = function () {}
166+
process.nextTick(() => {
167+
result.emit('error', new Error('Failed to obtain credentials'))
168+
})
169+
return result
170+
}
171+
}
172+
173+
const dd = fsAdmin.testMode
174+
? spawn('/bin/dd', ['of=' + filePath])
175+
: spawn('/usr/bin/pkexec', ['/bin/dd', 'of=' + filePath])
176+
177+
const stream = new EventEmitter()
178+
stream.write = (chunk, encoding, callback) => {
179+
dd.stdin.write(chunk, encoding, callback)
180+
}
181+
stream.end = (callback) => {
182+
if (callback) stream.on('finish', callback)
183+
dd.stdin.end()
184+
}
185+
dd.on('exit', (exitCode) => {
186+
if (exitCode !== 0) {
187+
stream.emit('error', new Error('dd exited with code ' + exitCode))
188+
}
189+
stream.emit('finish')
190+
})
191+
192+
return stream
193+
}
194+
})
195+
break
152196
}
153197

154198
function wrapCallback (commandName, callback) {

test/fs-admin.test.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ const fsAdmin = require('..')
77
// Comment this out to test with actual privilege escalation.
88
fsAdmin.testMode = true
99

10-
if (process.platform !== 'win32' && process.platform !== 'darwin') {
11-
process.exit(0)
12-
}
13-
1410
describe('fs-admin', function () {
1511
let dirPath, filePath
1612

@@ -23,7 +19,7 @@ describe('fs-admin', function () {
2319
if (!fsAdmin.testMode) this.timeout(10000)
2420

2521
describe('createWriteStream', () => {
26-
if (process.platform !== 'darwin') return
22+
if (process.platform === 'win32') return
2723

2824
it('writes to the given file as the admin user', (done) => {
2925
fs.writeFileSync(filePath, '')
@@ -74,6 +70,8 @@ describe('fs-admin', function () {
7470
})
7571

7672
describe('makeTree', () => {
73+
if (process.platform === 'linux') return
74+
7775
it('creates a directory at the given path as the admin user', (done) => {
7876
const pathToCreate = path.join(dirPath, 'dir1', 'dir2', 'dir3')
7977

@@ -92,6 +90,8 @@ describe('fs-admin', function () {
9290
})
9391

9492
describe('unlink', () => {
93+
if (process.platform === 'linux') return
94+
9595
it('deletes the given file as the admin user', (done) => {
9696
fs.writeFileSync(filePath, '')
9797

@@ -126,6 +126,8 @@ describe('fs-admin', function () {
126126
})
127127

128128
describe('symlink', () => {
129+
if (process.platform === 'linux') return
130+
129131
it('creates a symlink at the given path as the admin user', (done) => {
130132
fsAdmin.symlink(__filename, filePath, (error) => {
131133
assert.strictEqual(error, null)
@@ -141,6 +143,8 @@ describe('fs-admin', function () {
141143
})
142144

143145
describe('recursiveCopy', () => {
146+
if (process.platform === 'linux') return
147+
144148
it('copies the given folder to the given location as the admin user', (done) => {
145149
const sourcePath = path.join(dirPath, 'src-dir')
146150
fs.mkdirSync(sourcePath)

0 commit comments

Comments
 (0)