Skip to content

Commit cb297ca

Browse files
committedAug 17, 2017
Add uid/gid options to extract and unpack
Enables forcibly setting the ownership of all extracted items, regardless of the uid/gid of the process or the value in the archive. This also avoids calling chown on files and directories if they are already going to be owned by the proper uid/gid (that is, if the specified uid/gid matches the process uid and gid). Note that it does _not_ set the ownership of pre-existing folders that are already in place when the extract is performed. It only guarantees that files and directories created by the unpack will have their ownership set as specified. Thus it provides a reliable gaurantee of file ownership only if extracting into a previously empty directory. Fix #133
1 parent 38c265b commit cb297ca

File tree

4 files changed

+229
-28
lines changed

4 files changed

+229
-28
lines changed
 

‎README.md

+20
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,16 @@ The following options are supported:
295295
`tar(1)`, but ACLs and other system-specific data is never unpacked
296296
in this implementation, and modes are set by default already.
297297
[Alias: `p`]
298+
- `uid` Set to a number to force ownership of all extracted files and
299+
folders, and all implicitly created directories, to be owned by the
300+
specified user id, regardless of the `uid` field in the archive.
301+
Cannot be used along with `preserveOwner`. Requires also setting a
302+
`gid` option.
303+
- `gid` Set to a number to force ownership of all extracted files and
304+
folders, and all implicitly created directories, to be owned by the
305+
specified group id, regardless of the `gid` field in the archive.
306+
Cannot be used along with `preserveOwner`. Requires also setting a
307+
`uid` option.
298308

299309
The following options are mostly internal, but can be modified in some
300310
advanced use cases, such as re-using caches between runs.
@@ -554,6 +564,16 @@ mode.
554564
- `win32` True if on a windows platform. Causes behavior where
555565
filenames containing `<|>?` chars are converted to
556566
windows-compatible values while being unpacked.
567+
- `uid` Set to a number to force ownership of all extracted files and
568+
folders, and all implicitly created directories, to be owned by the
569+
specified user id, regardless of the `uid` field in the archive.
570+
Cannot be used along with `preserveOwner`. Requires also setting a
571+
`gid` option.
572+
- `gid` Set to a number to force ownership of all extracted files and
573+
folders, and all implicitly created directories, to be owned by the
574+
specified group id, regardless of the `gid` field in the archive.
575+
Cannot be used along with `preserveOwner`. Requires also setting a
576+
`uid` option.
557577

558578
### class tar.Unpack.Sync
559579

‎lib/mkdir.js

+38-19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const mkdirp = require('mkdirp')
44
const fs = require('fs')
55
const path = require('path')
6+
const chownr = require('chownr')
67

78
class SymlinkError extends Error {
89
constructor (symlink, path) {
@@ -23,17 +24,25 @@ const mkdir = module.exports = (dir, opt, cb) => {
2324
const mode = opt.mode | 0o0700
2425
const needChmod = (mode & umask) !== 0
2526

27+
const uid = opt.uid
28+
const gid = opt.gid
29+
const doChown = typeof uid === 'number' &&
30+
typeof gid === 'number' &&
31+
( uid !== opt.processUid || gid !== opt.processGid )
32+
2633
const preserve = opt.preserve
2734
const unlink = opt.unlink
2835
const cache = opt.cache
2936
const cwd = opt.cwd
3037

31-
const done = er => {
38+
const done = (er, created) => {
3239
if (er)
3340
cb(er)
3441
else {
3542
cache.set(dir, true)
36-
if (needChmod)
43+
if (created && doChown)
44+
chownr(created, uid, gid, er => done(er))
45+
else if (needChmod)
3746
fs.chmod(dir, mode, cb)
3847
else
3948
cb()
@@ -48,39 +57,41 @@ const mkdir = module.exports = (dir, opt, cb) => {
4857

4958
const sub = path.relative(cwd, dir)
5059
const parts = sub.split(/\/|\\/)
51-
mkdir_(cwd, parts, mode, cache, unlink, done)
60+
mkdir_(cwd, parts, mode, cache, unlink, null, done)
5261
}
5362

54-
const mkdir_ = (base, parts, mode, cache, unlink, cb) => {
63+
const mkdir_ = (base, parts, mode, cache, unlink, created, cb) => {
5564
if (!parts.length)
56-
return cb()
65+
return cb(null, created)
5766
const p = parts.shift()
5867
const part = base + '/' + p
5968
if (cache.get(part))
60-
return mkdir_(part, parts, mode, cache, unlink, cb)
61-
fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cb))
69+
return mkdir_(part, parts, mode, cache, unlink, created, cb)
70+
fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, created, cb))
6271
}
6372

64-
const onmkdir = (part, parts, mode, cache, unlink, cb) => er => {
73+
const onmkdir = (part, parts, mode, cache, unlink, created, cb) => er => {
6574
if (er) {
6675
fs.lstat(part, (statEr, st) => {
6776
if (statEr)
6877
cb(statEr)
6978
else if (st.isDirectory())
70-
mkdir_(part, parts, mode, cache, unlink, cb)
79+
mkdir_(part, parts, mode, cache, unlink, created, cb)
7180
else if (unlink)
7281
fs.unlink(part, er => {
7382
if (er)
7483
return cb(er)
75-
fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, cb))
84+
fs.mkdir(part, mode, onmkdir(part, parts, mode, cache, unlink, created, cb))
7685
})
7786
else if (st.isSymbolicLink())
7887
return cb(new SymlinkError(part, part + '/' + parts.join('/')))
7988
else
8089
cb(er)
8190
})
82-
} else
83-
mkdir_(part, parts, mode, cache, unlink, cb)
91+
} else {
92+
created = created || part
93+
mkdir_(part, parts, mode, cache, unlink, created, cb)
94+
}
8495
}
8596

8697
const mkdirSync = module.exports.sync = (dir, opt) => {
@@ -90,13 +101,21 @@ const mkdirSync = module.exports.sync = (dir, opt) => {
90101
const mode = opt.mode | 0o0700
91102
const needChmod = (mode & umask) !== 0
92103

104+
const uid = opt.uid
105+
const gid = opt.gid
106+
const doChown = typeof uid === 'number' &&
107+
typeof gid === 'number' &&
108+
( uid !== opt.processUid || gid !== opt.processGid )
109+
93110
const preserve = opt.preserve
94111
const unlink = opt.unlink
95112
const cache = opt.cache
96113
const cwd = opt.cwd
97114

98-
const done = er => {
115+
const done = (created) => {
99116
cache.set(dir, true)
117+
if (created && doChown)
118+
chownr.sync(created, uid, gid)
100119
if (needChmod)
101120
fs.chmodSync(dir, mode)
102121
cache.set(dir, true)
@@ -105,14 +124,12 @@ const mkdirSync = module.exports.sync = (dir, opt) => {
105124
if (cache && cache.get(dir) === true || dir === cwd)
106125
return done()
107126

108-
if (preserve) {
109-
mkdirp.sync(dir, mode)
110-
cache.set(dir, true)
111-
return
112-
}
127+
if (preserve)
128+
return done(mkdirp.sync(dir, mode))
113129

114130
const sub = path.relative(cwd, dir)
115131
const parts = sub.split(/\/|\\/)
132+
let created = null
116133
for (let p = parts.shift(), part = cwd;
117134
p && (part += '/' + p);
118135
p = parts.shift()) {
@@ -122,6 +139,7 @@ const mkdirSync = module.exports.sync = (dir, opt) => {
122139

123140
try {
124141
fs.mkdirSync(part, mode)
142+
created = created || part
125143
cache.set(part, true)
126144
} catch (er) {
127145
const st = fs.lstatSync(part)
@@ -131,12 +149,13 @@ const mkdirSync = module.exports.sync = (dir, opt) => {
131149
} else if (unlink) {
132150
fs.unlinkSync(part)
133151
fs.mkdirSync(part, mode)
152+
created = created || part
134153
cache.set(part, true)
135154
continue
136155
} else if (st.isSymbolicLink())
137156
return new SymlinkError(part, part + '/' + parts.join('/'))
138157
}
139158
}
140159

141-
return done()
160+
return done(created)
142161
}

‎lib/unpack.js

+51-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const ENDED = Symbol('ended')
2929
const MAYBECLOSE = Symbol('maybeClose')
3030
const SKIP = Symbol('skip')
3131
const DOCHOWN = Symbol('doChown')
32+
const UID = Symbol('uid')
33+
const GID = Symbol('gid')
3234

3335
class Unpack extends Parser {
3436
constructor (opt) {
@@ -50,14 +52,31 @@ class Unpack extends Parser {
5052

5153
this.dirCache = opt.dirCache || new Map()
5254

53-
if (opt.preserveOwner === undefined)
55+
if (typeof opt.uid === 'number' || typeof opt.gid === 'number') {
56+
// need both or neither
57+
if (typeof opt.uid !== 'number' || typeof opt.gid !== 'number')
58+
throw new TypeError('cannot set owner without number uid and gid')
59+
if (opt.preserveOwner)
60+
throw new TypeError(
61+
'cannot preserve owner in archive and also set owner explicitly')
62+
this.uid = opt.uid
63+
this.gid = opt.gid
64+
this.setOwner = true
65+
} else {
66+
this.uid = null
67+
this.gid = null
68+
this.setOwner = false
69+
}
70+
71+
// default true for root
72+
if (opt.preserveOwner === undefined && typeof opt.uid !== 'number')
5473
this.preserveOwner = process.getuid && process.getuid() === 0
5574
else
5675
this.preserveOwner = !!opt.preserveOwner
5776

58-
this.processUid = this.preserveOwner && process.getuid ?
77+
this.processUid = (this.preserveOwner || this.setOwner) && process.getuid ?
5978
process.getuid() : null
60-
this.processGid = this.preserveOwner && process.getgid ?
79+
this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ?
6180
process.getgid() : null
6281

6382
// turn ><?| in filenames into 0xf000-higher encoded forms
@@ -173,6 +192,10 @@ class Unpack extends Parser {
173192

174193
[MKDIR] (dir, mode, cb) {
175194
mkdir(dir, {
195+
uid: this.uid,
196+
gid: this.gid,
197+
processUid: this.processUid,
198+
processGid: this.processGid,
176199
umask: this.processUmask,
177200
preserve: this.preservePaths,
178201
unlink: this.unlink,
@@ -183,9 +206,26 @@ class Unpack extends Parser {
183206
}
184207

185208
[DOCHOWN] (entry) {
209+
// in preserve owner mode, chown if the entry doesn't match process
210+
// in set owner mode, chown if setting doesn't match process
186211
return this.preserveOwner &&
187212
( typeof entry.uid === 'number' && entry.uid !== this.processUid ||
188213
typeof entry.gid === 'number' && entry.gid !== this.processGid )
214+
||
215+
( typeof this.uid === 'number' && this.uid !== this.processUid ||
216+
typeof this.gid === 'number' && this.gid !== this.processGid )
217+
}
218+
219+
[UID] (entry) {
220+
return typeof this.uid === 'number' ? this.uid
221+
: typeof entry.uid === 'number' ? entry.uid
222+
: this.processUid
223+
}
224+
225+
[GID] (entry) {
226+
return typeof this.gid === 'number' ? this.gid
227+
: typeof entry.gid === 'number' ? entry.gid
228+
: this.processGid
189229
}
190230

191231
[FILE] (entry) {
@@ -208,7 +248,7 @@ class Unpack extends Parser {
208248
fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, cb))
209249
if (this[DOCHOWN](entry))
210250
queue.push(cb =>
211-
fs.chown(entry.absolute, entry.uid, entry.gid || this.processGid, cb))
251+
fs.chown(entry.absolute, this[UID](entry), this[GID](entry), cb))
212252
processQueue()
213253
})
214254
entry.pipe(stream)
@@ -236,7 +276,7 @@ class Unpack extends Parser {
236276
fs.utimes(entry.absolute, entry.atime || new Date(), entry.mtime, cb))
237277
if (this[DOCHOWN](entry))
238278
queue.push(cb =>
239-
fs.chown(entry.absolute, entry.uid, entry.gid || this.processGid, cb))
279+
fs.chown(entry.absolute, this[UID](entry), this[GID](entry), cb))
240280

241281
processQueue()
242282
})
@@ -375,7 +415,7 @@ class UnpackSync extends Unpack {
375415
}
376416
if (this[DOCHOWN](entry)) {
377417
try {
378-
fs.fchownSync(fd, entry.uid, entry.gid || this.processGid)
418+
fs.fchownSync(fd, this[UID](entry), this[GID](entry))
379419
} catch (er) {}
380420
}
381421
try { fs.closeSync(fd) } catch (er) { this[ONERROR](er, entry) }
@@ -395,7 +435,7 @@ class UnpackSync extends Unpack {
395435
}
396436
if (this[DOCHOWN](entry)) {
397437
try {
398-
fs.chownSync(entry.absolute, entry.uid, entry.gid || this.processGid)
438+
fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry))
399439
} catch (er) {}
400440
}
401441
entry.resume()
@@ -404,6 +444,10 @@ class UnpackSync extends Unpack {
404444
[MKDIR] (dir, mode) {
405445
try {
406446
return mkdir.sync(dir, {
447+
uid: this.uid,
448+
gid: this.gid,
449+
processUid: this.processUid,
450+
processGid: this.processGid,
407451
umask: this.processUmask,
408452
preserve: this.preservePaths,
409453
unlink: this.unlink,

‎test/unpack.js

+120-2
Original file line numberDiff line numberDiff line change
@@ -1497,6 +1497,13 @@ t.test('set owner', t => {
14971497
size: 3
14981498
},
14991499
'qux',
1500+
{
1501+
gid: 5108675309,
1502+
path: 'foo/different-gid-nouid/bar',
1503+
type: 'File',
1504+
size: 3
1505+
},
1506+
'qux',
15001507
{
15011508
uid: myUid,
15021509
gid: myGid,
@@ -1552,7 +1559,7 @@ t.test('set owner', t => {
15521559
called = 0
15531560
const u = new Unpack.Sync({ cwd: dir, preserveOwner: true })
15541561
u.end(data)
1555-
t.equal(called, 4, 'called chowns')
1562+
t.equal(called, 5, 'called chowns')
15561563
t.end()
15571564
})
15581565

@@ -1561,7 +1568,7 @@ t.test('set owner', t => {
15611568
const u = new Unpack({ cwd: dir, preserveOwner: true })
15621569
u.end(data)
15631570
u.on('close', _ => {
1564-
t.equal(called, 4, 'called chowns')
1571+
t.equal(called, 5, 'called chowns')
15651572
t.end()
15661573
})
15671574
})
@@ -1772,3 +1779,114 @@ t.test('use explicit chmod when required by umask', t => {
17721779
check(t)
17731780
})
17741781
})
1782+
1783+
t.test('chown implicit dirs and also the entries', t => {
1784+
const basedir = path.resolve(unpackdir, 'chownr')
1785+
1786+
// club these so that the test can run as non-root
1787+
const chown = fs.chown
1788+
const chownSync = fs.chownSync
1789+
1790+
const getuid = process.getuid
1791+
const getgid = process.getgid
1792+
t.teardown(_ => {
1793+
fs.chown = chown
1794+
fs.chownSync = chownSync
1795+
process.getgid = getgid
1796+
})
1797+
1798+
let chowns = 0
1799+
1800+
let currentTest = null
1801+
fs.chown = (path, uid, gid, cb) => {
1802+
currentTest.equal(uid, 420, 'chown(' + path + ') uid')
1803+
currentTest.equal(gid, 666, 'chown(' + path + ') gid')
1804+
chowns ++
1805+
cb()
1806+
}
1807+
1808+
fs.chownSync = (path, uid, gid) => {
1809+
currentTest.equal(uid, 420, 'chownSync(' + path + ') uid')
1810+
currentTest.equal(gid, 666, 'chownSync(' + path + ') gid')
1811+
chowns ++
1812+
}
1813+
1814+
fs.fchownSync = (path, uid, gid) => {
1815+
currentTest.equal(uid, 420, 'chownSync(' + path + ') uid')
1816+
currentTest.equal(gid, 666, 'chownSync(' + path + ') gid')
1817+
chowns ++
1818+
}
1819+
1820+
const data = makeTar([
1821+
{
1822+
path: 'a/b/c',
1823+
mode: 0o775,
1824+
type: 'File',
1825+
size: 1,
1826+
uid: null,
1827+
gid: null
1828+
},
1829+
'.',
1830+
{
1831+
path: 'x/y/z',
1832+
mode: 0o775,
1833+
uid: 12345,
1834+
gid: 54321,
1835+
type: 'File',
1836+
size: 1
1837+
},
1838+
'.',
1839+
'',
1840+
''
1841+
])
1842+
1843+
const check = t => {
1844+
currentTest = null
1845+
t.equal(chowns, 6)
1846+
chowns = 0
1847+
rimraf.sync(basedir)
1848+
t.end()
1849+
}
1850+
1851+
t.test('throws when setting uid/gid improperly', t => {
1852+
t.throws(_ => new Unpack({ uid: 420 }),
1853+
TypeError('cannot set owner without number uid and gid'))
1854+
t.throws(_ => new Unpack({ gid: 666 }),
1855+
TypeError('cannot set owner without number uid and gid'))
1856+
t.throws(_ => new Unpack({ uid: 1, gid: 2, preserveOwner: true }),
1857+
TypeError('cannot preserve owner in archive and also set owner explicitly'))
1858+
t.end()
1859+
})
1860+
1861+
const tests = () =>
1862+
t.test('async', t => {
1863+
currentTest = t
1864+
mkdirp.sync(basedir)
1865+
const unpack = new Unpack({ cwd: basedir, uid: 420, gid: 666 })
1866+
unpack.on('close', _ => check(t))
1867+
unpack.end(data)
1868+
}).then(t.test('sync', t => {
1869+
currentTest = t
1870+
mkdirp.sync(basedir)
1871+
const unpack = new Unpack.Sync({ cwd: basedir, uid: 420, gid: 666 })
1872+
unpack.end(data)
1873+
check(t)
1874+
}))
1875+
1876+
tests()
1877+
1878+
t.test('make it look like processUid is 420', t => {
1879+
process.getuid = () => 420
1880+
t.end()
1881+
})
1882+
1883+
tests()
1884+
1885+
t.test('make it look like processGid is 666', t => {
1886+
process.getuid = getuid
1887+
process.getgid = () => 666
1888+
t.end()
1889+
})
1890+
1891+
return tests()
1892+
})

0 commit comments

Comments
 (0)
Please sign in to comment.