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

feat: improve permissions support #1059

Merged
merged 16 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
12 changes: 8 additions & 4 deletions src/__tests__/promises.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,22 @@ describe('Promises API', () => {
});
});
describe('chmod(mode)', () => {
const vol = new Volume();
const { promises } = vol;
vol.fromJSON({
'/foo': 'bar',
let vol;
beforeEach(() => {
vol = new Volume();
vol.fromJSON({
'/foo': 'bar',
});
});
it('Change mode of existing file', async () => {
const { promises } = vol;
const fileHandle = await promises.open('/foo', 'a');
await fileHandle.chmod(0o444);
expect(vol.statSync('/foo').mode & 0o777).toEqual(0o444);
await fileHandle.close();
});
it('Reject when the file handle was closed', async () => {
const { promises } = vol;
const fileHandle = await promises.open('/foo', 'a');
await fileHandle.close();
return expect(fileHandle.chmod(0o666)).rejects.toBeInstanceOf(Error);
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { createFsFromVolume, Volume } from '..';
import { Link, Node } from '../node';

// Turn the done callback into an incremental one that will only fire after being called
// `times` times, failing with the first reported error if such exists.
// Useful for testing callback-style functions with several different fixtures without
// having to clutter the test suite with a multitude of individual tests (like it.each would).
export const multitest = (_done: (err?: Error) => void, times: number) => {
let err;
return function done(_err?: Error) {
err ??= _err;
if (!--times) _done(_err);
};
};

export const create = (json: { [s: string]: string } = { '/foo': 'bar' }) => {
const vol = Volume.fromJSON(json);
return vol;
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/volume/ReadStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,46 @@ describe('ReadStream', () => {
done();
});
});

it('should emit EACCES error when file has insufficient permissions', done => {
const fs = createFs({ '/test': 'test' });
fs.chmodSync('/test', 0o333); // wx
new fs.ReadStream('/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error when containing directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/foo', 0o666); // rw
new fs.ReadStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error when intermediate directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/', 0o666); // rw
new fs.ReadStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});
});
56 changes: 56 additions & 0 deletions src/__tests__/volume/WriteStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,60 @@ describe('WriteStream', () => {
done();
});
});

it('should emit EACCES error when file has insufficient permissions', done => {
const fs = createFs({ '/test': 'test' });
fs.chmodSync('/test', 0o555); // rx
new fs.WriteStream('/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for an existing file when containing directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/foo', 0o666); // rw
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for when intermediate directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/', 0o666); // rw
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for a non-existent file when containing directory has insufficient permissions', done => {
const fs = createFs({});
fs.mkdirSync('/foo', { mode: 0o555 }); // rx
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});
});
53 changes: 52 additions & 1 deletion src/__tests__/volume/appendFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create } from '../util';
import { create, multitest } from '../util';

describe('appendFile(file, data[, options], callback)', () => {
it('Simple write to non-existing file', done => {
Expand All @@ -15,4 +15,55 @@ describe('appendFile(file, data[, options], callback)', () => {
done();
});
});

it('Appending gives EACCES without sufficient permissions on the file', done => {
const vol = create({ '/foo': 'foo' });
vol.chmodSync('/foo', 0o555); // rx across the board
vol.appendFile('/foo', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});

it('Appending gives EACCES if file does not exist and containing directory has insufficient permissions', _done => {
const perms = [
0o555, // rx across the board
0o666, // rw across the board
];
const done = multitest(_done, perms.length);

perms.forEach(perm => {
const vol = create({});
vol.mkdirSync('/foo', { mode: perm });
vol.appendFile('/foo/test', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});
});

it('Appending gives EACCES if intermediate directory has insufficient permissions', done => {
const vol = create({});
vol.mkdirSync('/foo');
vol.chmodSync('/', 0o666); // rw
vol.appendFile('/foo/test', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});
});
29 changes: 29 additions & 0 deletions src/__tests__/volume/appendFileSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,33 @@ describe('appendFileSync(file, data, options)', () => {
vol.appendFileSync('/a', 'c');
expect(vol.readFileSync('/a', 'utf8')).toEqual('bc');
});
it('Appending throws EACCES without sufficient permissions on the file', () => {
const vol = create({ '/foo': 'foo' });
vol.chmodSync('/foo', 0o555); // rx across the board
expect(() => {
vol.appendFileSync('/foo', 'bar');
}).toThrowError(/EACCES/);
});
it('Appending throws EACCES if file does not exist and containing directory has insufficient permissions', () => {
const perms = [
0o555, // rx across the board
// 0o666, // rw across the board
// 0o111, // x
// 0o222 // w
];
perms.forEach(perm => {
const vol = create({});
vol.mkdirSync('/foo', perm);
expect(() => {
vol.appendFileSync('/foo/test', 'bar');
}).toThrowError(/EACCES/);
});
});
it('Appending throws EACCES if intermediate directory has insufficient permissions', () => {
const vol = create({ '/foo/test': 'test' });
vol.chmodSync('/', 0o666); // rw
expect(() => {
vol.appendFileSync('/foo/test', 'bar');
}).toThrowError(/EACCES/);
});
});
76 changes: 76 additions & 0 deletions src/__tests__/volume/chmodSync.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { create } from '../util';

describe('chmodSync', () => {
it('should be able to chmod files and directories owned by the UID regardless of their permissions', () => {
const perms = [
0o777, // rwx
0o666, // rw
0o555, // rx
0o444, // r
0o333, // wx
0o222, // w
0o111, // x
0o000, // none
];
// Check for directories
perms.forEach(perm => {
const vol = create({});
vol.mkdirSync('/foo', { mode: perm });
expect(() => {
vol.chmodSync('/foo', 0o777);
}).not.toThrow();
});
// Check for files
perms.forEach(perm => {
const vol = create({ '/foo': 'foo' });
expect(() => {
vol.chmodSync('/foo', 0o777);
}).not.toThrow();
});
});

it('should chmod the target of a symlink, not the symlink itself', () => {
const vol = create({ '/target': 'contents' });
vol.symlinkSync('/target', '/link');
const expectedLink = vol.lstatSync('/link').mode;
const expectedTarget = vol.statSync('/target').mode & ~0o777;
vol.chmodSync('/link', 0);

expect(vol.lstatSync('/link').mode).toEqual(expectedLink);
expect(vol.statSync('/target').mode).toEqual(expectedTarget);
});

it.skip('should throw EPERM when trying to chmod targets not owned by the uid', () => {
const uid = process.getuid() + 1;
// Check for directories
const vol = create({});
vol.mkdirSync('/foo');
vol.chownSync('/foo', uid, process.getgid());
expect(() => {
vol.chmodSync('/foo', 0o777);
}).toThrow(/PERM/);
});

it("should throw ENOENT when target doesn't exist", () => {
const vol = create({});
expect(() => {
vol.chmodSync('/foo', 0o777);
}).toThrow(/ENOENT/);
});

it('should throw EACCES when containing directory has insufficient permissions', () => {
const vol = create({ '/foo/test': 'test' });
vol.chmodSync('/foo', 0o666); // rw
expect(() => {
vol.chmodSync('/foo/test', 0o777);
}).toThrow(/EACCES/);
});

it('should throw EACCES when intermediate directory has insufficient permissions', () => {
const vol = create({ '/foo/test': 'test' });
vol.chmodSync('/', 0o666); // rw
expect(() => {
vol.chmodSync('/foo/test', 0o777);
}).toThrow(/EACCES/);
});
});
Loading
Loading