Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle permissions #7

Merged
merged 7 commits into from
Feb 4, 2014
Merged
Show file tree
Hide file tree
Changes from all 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
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