typescript-virtual-container 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +871 -1231
- package/benchmark-results.txt +21 -21
- package/biome.json +9 -0
- package/dist/SSHMimic/index.d.ts +19 -2
- package/dist/SSHMimic/index.d.ts.map +1 -1
- package/dist/SSHMimic/index.js +127 -15
- package/dist/VirtualFileSystem/index.d.ts +115 -88
- package/dist/VirtualFileSystem/index.d.ts.map +1 -1
- package/dist/VirtualFileSystem/index.js +406 -258
- package/dist/VirtualShell/index.d.ts +3 -4
- package/dist/VirtualShell/index.d.ts.map +1 -1
- package/dist/VirtualShell/index.js +5 -23
- package/dist/VirtualUserManager/index.d.ts +41 -3
- package/dist/VirtualUserManager/index.d.ts.map +1 -1
- package/dist/VirtualUserManager/index.js +83 -21
- package/dist/commands/chmod.d.ts +3 -0
- package/dist/commands/chmod.d.ts.map +1 -0
- package/dist/commands/chmod.js +31 -0
- package/dist/commands/cp.d.ts +3 -0
- package/dist/commands/cp.d.ts.map +1 -0
- package/dist/commands/cp.js +68 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.d.ts.map +1 -0
- package/dist/commands/find.js +48 -0
- package/dist/commands/grep.d.ts.map +1 -1
- package/dist/commands/grep.js +61 -35
- package/dist/commands/head.d.ts +3 -0
- package/dist/commands/head.d.ts.map +1 -0
- package/dist/commands/head.js +30 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +25 -35
- package/dist/commands/ln.d.ts +3 -0
- package/dist/commands/ln.d.ts.map +1 -0
- package/dist/commands/ln.js +42 -0
- package/dist/commands/mv.d.ts +3 -0
- package/dist/commands/mv.d.ts.map +1 -0
- package/dist/commands/mv.js +35 -0
- package/dist/commands/tail.d.ts +3 -0
- package/dist/commands/tail.d.ts.map +1 -0
- package/dist/commands/tail.js +33 -0
- package/dist/commands/wc.d.ts +3 -0
- package/dist/commands/wc.d.ts.map +1 -0
- package/dist/commands/wc.js +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/standalone.js +7 -9
- package/package.json +7 -3
- package/scripts/publish-package.sh +70 -0
- package/src/SSHMimic/index.ts +159 -17
- package/src/VirtualFileSystem/index.ts +500 -280
- package/src/VirtualShell/index.ts +5 -33
- package/src/VirtualUserManager/index.ts +92 -26
- package/src/commands/chmod.ts +33 -0
- package/src/commands/cp.ts +76 -0
- package/src/commands/find.ts +61 -0
- package/src/commands/grep.ts +54 -38
- package/src/commands/head.ts +35 -0
- package/src/commands/index.ts +25 -43
- package/src/commands/ln.ts +47 -0
- package/src/commands/mv.ts +43 -0
- package/src/commands/tail.ts +37 -0
- package/src/commands/wc.ts +48 -0
- package/src/index.ts +1 -0
- package/src/standalone.ts +12 -9
- package/standalone.js +102 -0
- package/standalone.js.map +7 -0
- package/tests/bun-test-shim.ts +1 -0
- package/tests/sftp.test.ts +115 -191
- package/tests/users.test.ts +66 -83
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from "vitest";
|
package/tests/sftp.test.ts
CHANGED
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
/// <reference types="bun" />
|
|
2
2
|
import { describe, expect, test } from "bun:test";
|
|
3
|
-
import { rmSync } from "node:fs";
|
|
4
3
|
import type { FileEntryWithStats, SFTPWrapper } from "ssh2";
|
|
5
4
|
import { Client } from "ssh2";
|
|
6
5
|
import { SftpMimic } from "../src/SSHMimic/sftp";
|
|
7
6
|
import VirtualFileSystem from "../src/VirtualFileSystem";
|
|
8
7
|
import { VirtualUserManager } from "../src/VirtualUserManager";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
return `./temp-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
12
|
-
}
|
|
9
|
+
// VirtualFileSystem is now pure in-memory — no temp dir or cleanup needed.
|
|
13
10
|
|
|
14
11
|
function connectSftp(port: number): Promise<{ client: Client; sftp: SFTPWrapper }> {
|
|
15
12
|
return new Promise((resolve, reject) => {
|
|
16
13
|
const client = new Client();
|
|
17
14
|
client.on("ready", () => {
|
|
18
15
|
client.sftp((err, sftp) => {
|
|
19
|
-
if (err) {
|
|
20
|
-
client.end();
|
|
21
|
-
reject(err);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
16
|
+
if (err) { client.end(); reject(err); return; }
|
|
24
17
|
resolve({ client, sftp });
|
|
25
18
|
});
|
|
26
19
|
});
|
|
27
|
-
|
|
28
20
|
client.on("error", reject);
|
|
29
21
|
client.connect({
|
|
30
22
|
host: "127.0.0.1",
|
|
31
23
|
port,
|
|
32
24
|
username: "root",
|
|
33
|
-
password: "
|
|
25
|
+
password: "", // root has no password — any value or empty is accepted
|
|
34
26
|
hostVerifier: () => true,
|
|
35
27
|
});
|
|
36
28
|
});
|
|
@@ -45,15 +37,10 @@ function connectSftpWithUser(
|
|
|
45
37
|
const client = new Client();
|
|
46
38
|
client.on("ready", () => {
|
|
47
39
|
client.sftp((err, sftp) => {
|
|
48
|
-
if (err) {
|
|
49
|
-
client.end();
|
|
50
|
-
reject(err);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
40
|
+
if (err) { client.end(); reject(err); return; }
|
|
53
41
|
resolve({ client, sftp });
|
|
54
42
|
});
|
|
55
43
|
});
|
|
56
|
-
|
|
57
44
|
client.on("error", reject);
|
|
58
45
|
client.connect({
|
|
59
46
|
host: "127.0.0.1",
|
|
@@ -67,253 +54,190 @@ function connectSftpWithUser(
|
|
|
67
54
|
|
|
68
55
|
describe("SftpMimic", () => {
|
|
69
56
|
test("authenticates with VirtualUserManager and serves files from the VirtualFileSystem", async () => {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const users = new VirtualUserManager(vfs, "root");
|
|
57
|
+
const vfs = new VirtualFileSystem();
|
|
58
|
+
const users = new VirtualUserManager(vfs);
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
vfs.writeFile(`${rootPath}/TEST.txt`, "hello world");
|
|
82
|
-
|
|
83
|
-
const server = new SftpMimic({
|
|
84
|
-
port: 0,
|
|
85
|
-
hostname: "test-sftp",
|
|
86
|
-
vfs,
|
|
87
|
-
users,
|
|
88
|
-
});
|
|
89
|
-
const port = await server.start();
|
|
60
|
+
await users.initialize();
|
|
61
|
+
|
|
62
|
+
const rootPath = "/home/root";
|
|
63
|
+
if (!vfs.exists(rootPath)) {
|
|
64
|
+
vfs.mkdir(rootPath, 0o755);
|
|
65
|
+
}
|
|
66
|
+
vfs.writeFile(`${rootPath}/TEST.txt`, "hello world");
|
|
90
67
|
|
|
68
|
+
const server = new SftpMimic({ port: 0, hostname: "test-sftp", vfs, users });
|
|
69
|
+
const port = await server.start();
|
|
70
|
+
|
|
71
|
+
try {
|
|
91
72
|
const { client, sftp } = await connectSftp(port);
|
|
73
|
+
|
|
92
74
|
const list = await new Promise<FileEntryWithStats[]>((resolve, reject) => {
|
|
93
|
-
sftp.readdir(
|
|
94
|
-
|
|
95
|
-
(
|
|
96
|
-
|
|
97
|
-
reject(err);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
resolve(list || []);
|
|
101
|
-
},
|
|
102
|
-
);
|
|
75
|
+
sftp.readdir("/home/root", (err?: Error | null, list?: FileEntryWithStats[]) => {
|
|
76
|
+
if (err) { reject(err); return; }
|
|
77
|
+
resolve(list || []);
|
|
78
|
+
});
|
|
103
79
|
});
|
|
104
80
|
|
|
105
81
|
expect(list.map((entry) => entry.filename)).toContain("TEST.txt");
|
|
106
82
|
|
|
107
83
|
const content = await new Promise<string>((resolve, reject) => {
|
|
108
|
-
sftp.readFile(
|
|
109
|
-
|
|
110
|
-
"utf8"
|
|
111
|
-
|
|
112
|
-
if (err) {
|
|
113
|
-
reject(err);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
resolve((data || Buffer.alloc(0)).toString("utf8"));
|
|
117
|
-
},
|
|
118
|
-
);
|
|
84
|
+
sftp.readFile("/home/root/TEST.txt", "utf8", (err?: Error | null, data?: Buffer) => {
|
|
85
|
+
if (err) { reject(err); return; }
|
|
86
|
+
resolve((data || Buffer.alloc(0)).toString("utf8"));
|
|
87
|
+
});
|
|
119
88
|
});
|
|
120
89
|
|
|
121
90
|
expect(content).toBe("hello world");
|
|
122
91
|
client.end();
|
|
123
|
-
server.stop();
|
|
124
92
|
} finally {
|
|
125
|
-
|
|
93
|
+
server.stop();
|
|
126
94
|
}
|
|
127
95
|
});
|
|
128
96
|
|
|
129
97
|
test("blocks path traversal attempts outside home directory", async () => {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const users = new VirtualUserManager(vfs, "root");
|
|
98
|
+
const vfs = new VirtualFileSystem();
|
|
99
|
+
const users = new VirtualUserManager(vfs);
|
|
133
100
|
|
|
134
|
-
|
|
135
|
-
await users.initialize();
|
|
136
|
-
|
|
137
|
-
const rootPath = "/home/root";
|
|
138
|
-
if (!vfs.exists(rootPath)) {
|
|
139
|
-
vfs.mkdir(rootPath, 0o755);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const server = new SftpMimic({
|
|
143
|
-
port: 0,
|
|
144
|
-
hostname: "test-sftp",
|
|
145
|
-
vfs,
|
|
146
|
-
users,
|
|
147
|
-
});
|
|
148
|
-
const port = await server.start();
|
|
101
|
+
await users.initialize();
|
|
149
102
|
|
|
103
|
+
const rootPath = "/home/root";
|
|
104
|
+
if (!vfs.exists(rootPath)) {
|
|
105
|
+
vfs.mkdir(rootPath, 0o755);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const server = new SftpMimic({ port: 0, hostname: "test-sftp", vfs, users });
|
|
109
|
+
const port = await server.start();
|
|
110
|
+
|
|
111
|
+
try {
|
|
150
112
|
const { client, sftp } = await connectSftp(port);
|
|
151
113
|
|
|
152
|
-
//
|
|
114
|
+
// /etc/passwd is outside /home/root — should be rejected
|
|
153
115
|
const traversalAttempt = await new Promise<Error | null>((resolve) => {
|
|
154
|
-
sftp.stat(
|
|
155
|
-
"/etc/passwd",
|
|
156
|
-
(err?: Error | null) => {
|
|
157
|
-
resolve(err ?? null);
|
|
158
|
-
},
|
|
159
|
-
);
|
|
116
|
+
sftp.stat("/etc/passwd", (err?: Error | null) => { resolve(err ?? null); });
|
|
160
117
|
});
|
|
161
118
|
|
|
162
119
|
expect(traversalAttempt).not.toBeNull();
|
|
163
120
|
expect(traversalAttempt?.message).toContain("Permission denied");
|
|
164
121
|
|
|
165
|
-
//
|
|
166
|
-
const homeAccess = await new Promise<FileEntryWithStats[]>((
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"/home/root",
|
|
172
|
-
(err?: Error | null, list?: FileEntryWithStats[]) => {
|
|
173
|
-
if (err) {
|
|
174
|
-
reject(err);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
resolve(list || []);
|
|
178
|
-
},
|
|
179
|
-
);
|
|
122
|
+
// /home/root itself should work
|
|
123
|
+
const homeAccess = await new Promise<FileEntryWithStats[]>((resolve, reject) => {
|
|
124
|
+
sftp.readdir("/home/root", (err?: Error | null, list?: FileEntryWithStats[]) => {
|
|
125
|
+
if (err) { reject(err); return; }
|
|
126
|
+
resolve(list || []);
|
|
127
|
+
});
|
|
180
128
|
});
|
|
181
|
-
|
|
182
129
|
expect(homeAccess).toBeDefined();
|
|
183
130
|
|
|
184
|
-
//
|
|
131
|
+
// Path traversal via ../.. should also be rejected
|
|
185
132
|
const upTraversalAttempt = await new Promise<Error | null>((resolve) => {
|
|
186
|
-
sftp.readdir(
|
|
187
|
-
"/home/root/../../etc",
|
|
188
|
-
(err?: Error | null) => {
|
|
189
|
-
resolve(err ?? null);
|
|
190
|
-
},
|
|
191
|
-
);
|
|
133
|
+
sftp.readdir("/home/root/../../etc", (err?: Error | null) => { resolve(err ?? null); });
|
|
192
134
|
});
|
|
193
|
-
|
|
194
135
|
expect(upTraversalAttempt).not.toBeNull();
|
|
195
136
|
|
|
196
137
|
client.end();
|
|
197
|
-
server.stop();
|
|
198
138
|
} finally {
|
|
199
|
-
|
|
139
|
+
server.stop();
|
|
200
140
|
}
|
|
201
141
|
});
|
|
202
142
|
|
|
203
|
-
test("
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
const users = new VirtualUserManager(vfs, "testpass");
|
|
143
|
+
test("allows a user with a password to authenticate", async () => {
|
|
144
|
+
const vfs = new VirtualFileSystem();
|
|
145
|
+
const users = new VirtualUserManager(vfs);
|
|
146
|
+
|
|
208
147
|
await users.initialize();
|
|
148
|
+
await users.addUser("alice", "alice-pass");
|
|
209
149
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
expect(passwordValid).toBe(true);
|
|
150
|
+
// Ensure alice's home exists (addUser should create it, but let's be explicit)
|
|
151
|
+
if (!vfs.exists("/home/alice")) {
|
|
152
|
+
vfs.mkdir("/home/alice", 0o755);
|
|
153
|
+
}
|
|
154
|
+
vfs.writeFile("/home/alice/hello.txt", "hi alice");
|
|
216
155
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
expect(vfs.exists(homePath)).toBe(true);
|
|
156
|
+
const server = new SftpMimic({ port: 0, hostname: "test-sftp", vfs, users });
|
|
157
|
+
const port = await server.start();
|
|
220
158
|
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
159
|
+
try {
|
|
160
|
+
const { client, sftp } = await connectSftpWithUser(port, "alice", "alice-pass");
|
|
161
|
+
|
|
162
|
+
const list = await new Promise<FileEntryWithStats[]>((resolve, reject) => {
|
|
163
|
+
sftp.readdir("/home/alice", (err?: Error | null, list?: FileEntryWithStats[]) => {
|
|
164
|
+
if (err) { reject(err); return; }
|
|
165
|
+
resolve(list || []);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(list.map((e) => e.filename)).toContain("hello.txt");
|
|
225
170
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
171
|
+
client.end();
|
|
172
|
+
} finally {
|
|
173
|
+
server.stop();
|
|
174
|
+
}
|
|
229
175
|
});
|
|
230
176
|
|
|
231
|
-
test("
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
177
|
+
test("rejects a user with a wrong password", async () => {
|
|
178
|
+
const vfs = new VirtualFileSystem();
|
|
179
|
+
const users = new VirtualUserManager(vfs);
|
|
180
|
+
|
|
181
|
+
await users.initialize();
|
|
182
|
+
await users.addUser("bob", "correct-pass");
|
|
183
|
+
|
|
184
|
+
const server = new SftpMimic({ port: 0, hostname: "test-sftp", vfs, users });
|
|
185
|
+
const port = await server.start();
|
|
235
186
|
|
|
236
187
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Ensure a deterministic password for environments where user data may persist.
|
|
247
|
-
await users.setPassword(currentUser, "root");
|
|
248
|
-
|
|
249
|
-
const server = new SftpMimic({
|
|
250
|
-
port: 0,
|
|
251
|
-
hostname: "test-sftp",
|
|
252
|
-
vfs,
|
|
253
|
-
users,
|
|
254
|
-
});
|
|
255
|
-
const port = await server.start();
|
|
188
|
+
const connectPromise = connectSftpWithUser(port, "bob", "wrong-pass");
|
|
189
|
+
await expect(connectPromise).rejects.toThrow();
|
|
190
|
+
} finally {
|
|
191
|
+
server.stop();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
256
194
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
currentUser,
|
|
261
|
-
"root",
|
|
262
|
-
);
|
|
195
|
+
test("allows writing and reading back a file over SFTP", async () => {
|
|
196
|
+
const vfs = new VirtualFileSystem();
|
|
197
|
+
const users = new VirtualUserManager(vfs);
|
|
263
198
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (err) {
|
|
270
|
-
reject(err);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
resolve(list || []);
|
|
274
|
-
},
|
|
275
|
-
);
|
|
276
|
-
});
|
|
199
|
+
await users.initialize();
|
|
200
|
+
|
|
201
|
+
if (!vfs.exists("/home/root")) {
|
|
202
|
+
vfs.mkdir("/home/root", 0o755);
|
|
203
|
+
}
|
|
277
204
|
|
|
278
|
-
|
|
279
|
-
|
|
205
|
+
const server = new SftpMimic({ port: 0, hostname: "test-sftp", vfs, users });
|
|
206
|
+
const port = await server.start();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const { client, sftp } = await connectSftp(port);
|
|
280
210
|
|
|
281
|
-
// Create a file as the system user
|
|
282
211
|
await new Promise<void>((resolve, reject) => {
|
|
283
212
|
sftp.writeFile(
|
|
284
|
-
|
|
285
|
-
Buffer.from("
|
|
213
|
+
"/home/root/written.txt",
|
|
214
|
+
Buffer.from("written via sftp"),
|
|
286
215
|
(err?: Error | null) => {
|
|
287
|
-
if (err) {
|
|
288
|
-
reject(err);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
216
|
+
if (err) { reject(err); return; }
|
|
291
217
|
resolve();
|
|
292
218
|
},
|
|
293
219
|
);
|
|
294
220
|
});
|
|
295
221
|
|
|
296
|
-
// Read the file back
|
|
297
222
|
const content = await new Promise<string>((resolve, reject) => {
|
|
298
223
|
sftp.readFile(
|
|
299
|
-
|
|
224
|
+
"/home/root/written.txt",
|
|
300
225
|
"utf8",
|
|
301
226
|
(err?: Error | null, data?: Buffer) => {
|
|
302
|
-
if (err) {
|
|
303
|
-
reject(err);
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
227
|
+
if (err) { reject(err); return; }
|
|
306
228
|
resolve((data || Buffer.alloc(0)).toString("utf8"));
|
|
307
229
|
},
|
|
308
230
|
);
|
|
309
231
|
});
|
|
310
232
|
|
|
311
|
-
expect(content).toBe("
|
|
233
|
+
expect(content).toBe("written via sftp");
|
|
234
|
+
|
|
235
|
+
// Also verify it landed in the in-memory VFS
|
|
236
|
+
expect(vfs.readFile("/home/root/written.txt")).toBe("written via sftp");
|
|
312
237
|
|
|
313
238
|
client.end();
|
|
314
|
-
server.stop();
|
|
315
239
|
} finally {
|
|
316
|
-
|
|
240
|
+
server.stop();
|
|
317
241
|
}
|
|
318
242
|
});
|
|
319
243
|
});
|
package/tests/users.test.ts
CHANGED
|
@@ -1,114 +1,97 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
2
|
import VirtualFileSystem from "../src/VirtualFileSystem";
|
|
6
3
|
import { VirtualUserManager } from "../src/VirtualUserManager";
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const tempDir = await mkdtemp(join(tmpdir(), "virtual-env-js-test-"));
|
|
12
|
-
try {
|
|
13
|
-
const vfs = new VirtualFileSystem(tempDir);
|
|
14
|
-
await vfs.restoreMirror();
|
|
15
|
-
await run(vfs);
|
|
16
|
-
} finally {
|
|
17
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
18
|
-
}
|
|
5
|
+
// VirtualFileSystem is now pure in-memory — no temp dir needed
|
|
6
|
+
function makeVfs(): VirtualFileSystem {
|
|
7
|
+
return new VirtualFileSystem();
|
|
19
8
|
}
|
|
20
9
|
|
|
21
10
|
describe("VirtualUserManager auto sudo", () => {
|
|
22
11
|
test("adds new users to sudoers by default", async () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
const vfs = makeVfs();
|
|
13
|
+
const users = new VirtualUserManager(vfs);
|
|
14
|
+
await users.initialize();
|
|
15
|
+
await users.addUser("alice", "alice-pass");
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
});
|
|
17
|
+
expect(users.isSudoer("alice")).toBe(true);
|
|
30
18
|
});
|
|
31
19
|
|
|
32
20
|
test("does not auto-add sudoers when disabled", async () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
const vfs = makeVfs();
|
|
22
|
+
const users = new VirtualUserManager(vfs, false);
|
|
23
|
+
await users.initialize();
|
|
24
|
+
await users.addUser("bob", "bob-pass");
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
});
|
|
26
|
+
expect(users.isSudoer("bob")).toBe(false);
|
|
40
27
|
});
|
|
41
28
|
|
|
42
29
|
test("updates password for existing user", async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
const vfs = makeVfs();
|
|
31
|
+
const users = new VirtualUserManager(vfs);
|
|
32
|
+
await users.initialize();
|
|
33
|
+
await users.addUser("alice", "alice-pass");
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
await users.setPassword("alice", "new-pass");
|
|
49
36
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
});
|
|
37
|
+
expect(users.verifyPassword("alice", "new-pass")).toBe(true);
|
|
38
|
+
expect(users.verifyPassword("alice", "alice-pass")).toBe(false);
|
|
53
39
|
});
|
|
54
40
|
});
|
|
55
41
|
|
|
56
42
|
describe("VirtualUserManager quotas", () => {
|
|
57
43
|
test("enforces quota for writes inside user home", async () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
44
|
+
const vfs = makeVfs();
|
|
45
|
+
const users = new VirtualUserManager(vfs);
|
|
46
|
+
await users.initialize();
|
|
47
|
+
await users.addUser("alice", "alice-pass");
|
|
48
|
+
const startingUsage = users.getUsageBytes("alice");
|
|
49
|
+
await users.setQuotaBytes("alice", startingUsage + 5);
|
|
50
|
+
|
|
51
|
+
expect(() => {
|
|
52
|
+
users.assertWriteWithinQuota("alice", "/home/alice/note.txt", "hello");
|
|
53
|
+
}).not.toThrow();
|
|
54
|
+
|
|
55
|
+
vfs.writeFile("/home/alice/note.txt", "hello");
|
|
56
|
+
|
|
57
|
+
expect(() => {
|
|
58
|
+
users.assertWriteWithinQuota(
|
|
59
|
+
"alice",
|
|
60
|
+
"/home/alice/note.txt",
|
|
61
|
+
"this exceeds the configured quota",
|
|
62
|
+
);
|
|
63
|
+
}).toThrow("quota exceeded for 'alice'");
|
|
79
64
|
});
|
|
80
65
|
|
|
81
66
|
test("does not enforce home quota outside user home", async () => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
});
|
|
67
|
+
const vfs = makeVfs();
|
|
68
|
+
const users = new VirtualUserManager(vfs);
|
|
69
|
+
await users.initialize();
|
|
70
|
+
await users.addUser("bob", "bob-pass");
|
|
71
|
+
await users.setQuotaBytes("bob", 1);
|
|
72
|
+
|
|
73
|
+
expect(() => {
|
|
74
|
+
users.assertWriteWithinQuota("bob", "/tmp/shared.txt", "large-content");
|
|
75
|
+
}).not.toThrow();
|
|
92
76
|
});
|
|
93
77
|
|
|
94
78
|
test("clearQuota removes enforced limit", async () => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
79
|
+
const vfs = makeVfs();
|
|
80
|
+
const users = new VirtualUserManager(vfs);
|
|
81
|
+
await users.initialize();
|
|
82
|
+
await users.addUser("charlie", "charlie-pass");
|
|
83
|
+
await users.setQuotaBytes("charlie", 2);
|
|
84
|
+
|
|
85
|
+
expect(users.getQuotaBytes("charlie")).toBe(2);
|
|
86
|
+
await users.clearQuota("charlie");
|
|
87
|
+
expect(users.getQuotaBytes("charlie")).toBeNull();
|
|
88
|
+
|
|
89
|
+
expect(() => {
|
|
90
|
+
users.assertWriteWithinQuota(
|
|
91
|
+
"charlie",
|
|
92
|
+
"/home/charlie/file.txt",
|
|
93
|
+
"long-content",
|
|
94
|
+
);
|
|
95
|
+
}).not.toThrow();
|
|
113
96
|
});
|
|
114
97
|
});
|