Skip to content

Commit

Permalink
Merge pull request #7 from tschaub/permission
Browse files Browse the repository at this point in the history
Handle permissions on POSIX-compliant systems.
  • Loading branch information
tschaub committed Feb 4, 2014
2 parents 61a2c84 + b2ad517 commit 1cabbb4
Show file tree
Hide file tree
Showing 7 changed files with 470 additions and 10 deletions.
20 changes: 16 additions & 4 deletions lib/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,21 @@ Binding.prototype.open = function(pathname, flags, mode, callback) {
throw new FSError('ENOTDIR', pathname);
}
item = new File();
item.setMode(mode);
if (mode) {
item.setMode(mode);
}
parent.addItem(path.basename(pathname), item);
}
if (descriptor.isRead() && !item) {
throw new FSError('ENOENT', pathname);
if (descriptor.isRead()) {
if (!item) {
throw new FSError('ENOENT', pathname);
}
if (!item.canRead()) {
throw new FSError('EACCES', pathname);
}
}
if (descriptor.isWrite() && !item.canWrite()) {
throw new FSError('EACCES', pathname);
}
if (descriptor.isTruncate()) {
item.setContent('');
Expand Down Expand Up @@ -506,7 +516,9 @@ Binding.prototype.mkdir = function(pathname, mode, callback) {
throw new FSError('ENOENT', pathname);
}
var dir = new Directory();
dir.setMode(mode);
if (mode) {
dir.setMode(mode);
}
parent.addItem(path.basename(pathname), dir);
});
};
Expand Down
5 changes: 5 additions & 0 deletions lib/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ function Directory() {
*/
this._items = {};

/**
* Permissions.
*/
this._mode = 0777;

}
util.inherits(Directory, Item);

Expand Down
14 changes: 11 additions & 3 deletions lib/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var path = require('path');

var Directory = require('./directory');
var File = require('./file');
var FSError = require('./error');
var SymbolicLink = require('./symlink');


Expand Down Expand Up @@ -69,9 +70,18 @@ function FileSystem() {
*/
FileSystem.prototype.getItem = function(filepath) {
var parts = getPathParts(filepath);
var currentParts = getPathParts(process.cwd());
var item = this._root;
var name;
for (var i = 0, ii = parts.length; i < ii; ++i) {
item = item.getItem(parts[i]);
name = parts[i];
if (item instanceof Directory && name !== currentParts[i]) {
// make sure traversal is allowed
if (!item.canExecute()) {
throw new FSError('EACCES', filepath);
}
}
item = item.getItem(name);
if (!item) {
break;
}
Expand Down Expand Up @@ -233,8 +243,6 @@ FileSystem.directory = function(config) {
var dir = new Directory();
if (config.hasOwnProperty('mode')) {
dir.setMode(config.mode);
} else {
dir.setMode(0777);
}
if (config.hasOwnProperty('uid')) {
dir.setUid(config.uid);
Expand Down
94 changes: 91 additions & 3 deletions lib/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ var path = require('path');
var counter = 0;


/**
* Permissions.
* @enum {number}
*/
var permissions = {
USER_READ: 256, // 0400
USER_WRITE: 128, // 0200
USER_EXEC: 64, // 0100
GROUP_READ: 32, // 0040
GROUP_WRITE: 16, // 0020
GROUP_EXEC: 8, // 0010
OTHER_READ: 4, // 0004
OTHER_WRITE: 2, // 0002
OTHER_EXEC: 1 // 0001
};

function getUid() {
return process.getuid && process.getuid();
}

function getGid() {
return process.getgid && process.getgid();
}



/**
* A filesystem item.
Expand Down Expand Up @@ -39,13 +64,13 @@ function Item() {
* User id.
* @type {number}
*/
this._uid = process.getuid ? process.getuid() : undefined;
this._uid = getUid();

/**
* Group id.
* @type {number}
*/
this._gid = process.getgid ? process.getgid() : undefined;
this._gid = getGid();

/**
* Item number.
Expand All @@ -61,6 +86,69 @@ function Item() {
}


/**
* Determine if the current user has read permission.
* @return {boolean} The current user can read.
*/
Item.prototype.canRead = function() {
var uid = getUid();
var gid = getGid();
var can = false;
if (uid === 0) {
can = true;
} else if (uid === this._uid) {
can = (permissions.USER_READ & this._mode) === permissions.USER_READ;
} else if (gid === this._gid) {
can = (permissions.GROUP_READ & this._mode) === permissions.GROUP_READ;
} else {
can = (permissions.OTHER_READ & this._mode) === permissions.OTHER_READ;
}
return can;
};


/**
* Determine if the current user has write permission.
* @return {boolean} The current user can write.
*/
Item.prototype.canWrite = function() {
var uid = getUid();
var gid = getGid();
var can = false;
if (uid === 0) {
can = true;
} else if (uid === this._uid) {
can = (permissions.USER_WRITE & this._mode) === permissions.USER_WRITE;
} else if (gid === this._gid) {
can = (permissions.GROUP_WRITE & this._mode) === permissions.GROUP_WRITE;
} else {
can = (permissions.OTHER_WRITE & this._mode) === permissions.OTHER_WRITE;
}
return can;
};


/**
* Determine if the current user has execute permission.
* @return {boolean} The current user can execute.
*/
Item.prototype.canExecute = function() {
var uid = getUid();
var gid = getGid();
var can = false;
if (uid === 0) {
can = true;
} else if (uid === this._uid) {
can = (permissions.USER_EXEC & this._mode) === permissions.USER_EXEC;
} else if (gid === this._gid) {
can = (permissions.GROUP_EXEC & this._mode) === permissions.GROUP_EXEC;
} else {
can = (permissions.OTHER_EXEC & this._mode) === permissions.OTHER_EXEC;
}
return can;
};


/**
* Get access time.
* @return {Date} Access time.
Expand Down Expand Up @@ -187,7 +275,7 @@ Item.prototype.getStats = function() {
ino: this._id,
atime: this.getATime(),
mtime: this.getMTime(),
ctime: this.getCTime(),
ctime: this.getCTime()
};
};

Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ The following [`fs` functions](http://nodejs.org/api/fs.html) are overridden: `f

Mock `fs.Stats` objects have the following properties: `dev`, `ino`, `nlink`, `mode`, `size`, `rdev`, `blksize`, `blocks`, `atime`, `ctime`, `mtime`, `uid`, and `gid`. In addition, all of the `is*()` method are provided (e.g. `isDirectory()`, `isFile()`, et al.).

Mock file access is controlled based on file mode where `process.getuid()` and `process.getgid()` are available (POSIX systems). On other systems (e.g. Windows) the file mode has no effect.

The following `fs` functions are *not* currently mocked (if your tests use these, they will work against the real file system): `fs.FSWatcher`, `fs.unwatchFile`, `fs.watch`, and `fs.watchFile`. Pull requests welcome.

Tested on Linux, OSX, and Windows using Node 0.8, 0.9, 0.10, and 0.11. Check the tickets for a list of [known issues](https://github.com/tschaub/mock-fs/issues).
Expand Down
102 changes: 102 additions & 0 deletions test/lib/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2503,4 +2503,106 @@ describe('Mocking the file system', function() {

});

if (process.getuid && process.getgid) {

describe('security', function() {

afterEach(mock.restore);

it('denies dir listing without execute on parent', function() {

mock({
secure: mock.directory({
mode: 0666,
items: {
insecure: ({
file: 'file content'
})
}
})
});

var err;
try {
fs.readdirSync('secure/insecure');
} catch (e) {
err = e;
}
assert.instanceOf(err, Error);
assert.equal(err.code, 'EACCES');

});

it('denies file read without execute on parent', function() {

mock({
secure: mock.directory({
mode: 0666,
items: {
insecure: ({
file: 'file content'
})
}
})
});

var err;
try {
fs.readFileSync('secure/insecure/file');
} catch (e) {
err = e;
}
assert.instanceOf(err, Error);
assert.equal(err.code, 'EACCES');

});

it('denies file read without read on file', function() {

mock({
insecure: ({
'write-only': mock.file({
mode: 0222,
content: 'write only'
})
})
});

var err;
try {
fs.readFileSync('insecure/write-only');
} catch (e) {
err = e;
}
assert.instanceOf(err, Error);
assert.equal(err.code, 'EACCES');

});

it('denies file write without write on file', function() {

mock({
insecure: ({
'read-only': mock.file({
mode: 0444,
content: 'read only'
})
})
});

var err;
try {
fs.writeFileSync('insecure/read-only', 'denied');
} catch (e) {
err = e;
}
assert.instanceOf(err, Error);
assert.equal(err.code, 'EACCES');

});

});

}

});
Loading

0 comments on commit 1cabbb4

Please sign in to comment.