@qodalis/cli-files 2.0.0-beta.13 → 2.0.0-beta.14
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/package.json +2 -2
- package/public-api.d.mts +86 -2
- package/public-api.d.ts +86 -2
- package/public-api.js +308 -17
- package/public-api.js.map +1 -1
- package/public-api.mjs +303 -19
- package/public-api.mjs.map +1 -1
- package/umd/index.global.js +178 -15
package/public-api.js
CHANGED
|
@@ -6,7 +6,7 @@ var cliCore = require('@qodalis/cli-core');
|
|
|
6
6
|
var IFileSystemService_TOKEN = "cli-file-system-service";
|
|
7
7
|
|
|
8
8
|
// src/lib/version.ts
|
|
9
|
-
var LIBRARY_VERSION = "2.0.0-beta.
|
|
9
|
+
var LIBRARY_VERSION = "2.0.0-beta.14";
|
|
10
10
|
var API_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/lib/processors/cli-ls-command-processor.ts
|
|
@@ -472,6 +472,14 @@ var CliMvCommandProcessor = class {
|
|
|
472
472
|
return raw.split(/\s+/).filter((t) => t && !t.startsWith("--") && !t.startsWith("-"));
|
|
473
473
|
}
|
|
474
474
|
};
|
|
475
|
+
function readableChildren(fs, path) {
|
|
476
|
+
try {
|
|
477
|
+
return fs.listDirectory(path);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
if (cliCore.isCliAccessDeniedError(e)) return null;
|
|
480
|
+
throw e;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
475
483
|
var CliTreeCommandProcessor = class {
|
|
476
484
|
constructor() {
|
|
477
485
|
this.command = "tree";
|
|
@@ -518,7 +526,8 @@ var CliTreeCommandProcessor = class {
|
|
|
518
526
|
)
|
|
519
527
|
);
|
|
520
528
|
const counts = { dirs: 0, files: 0 };
|
|
521
|
-
this.
|
|
529
|
+
this.fs = fs;
|
|
530
|
+
this.printTree(node, "", true, 0, maxDepth, context, counts, resolvedPath);
|
|
522
531
|
context.writer.writeln();
|
|
523
532
|
context.writer.writeln(
|
|
524
533
|
`${counts.dirs} directories, ${counts.files} files`
|
|
@@ -527,11 +536,15 @@ var CliTreeCommandProcessor = class {
|
|
|
527
536
|
context.writer.writeError(e.message);
|
|
528
537
|
}
|
|
529
538
|
}
|
|
530
|
-
printTree(node, prefix, isRoot, depth, maxDepth, context, counts) {
|
|
531
|
-
if (
|
|
539
|
+
printTree(node, prefix, isRoot, depth, maxDepth, context, counts, path = "/") {
|
|
540
|
+
if (depth >= maxDepth) {
|
|
532
541
|
return;
|
|
533
542
|
}
|
|
534
|
-
const
|
|
543
|
+
const visible = this.fs ? readableChildren(this.fs, path) : node.children;
|
|
544
|
+
if (!visible) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const children = [...visible].sort((a, b) => {
|
|
535
548
|
if (a.type !== b.type) {
|
|
536
549
|
return a.type === "directory" ? -1 : 1;
|
|
537
550
|
}
|
|
@@ -556,7 +569,8 @@ var CliTreeCommandProcessor = class {
|
|
|
556
569
|
depth + 1,
|
|
557
570
|
maxDepth,
|
|
558
571
|
context,
|
|
559
|
-
counts
|
|
572
|
+
counts,
|
|
573
|
+
path === "/" ? `/${child.name}` : `${path}/${child.name}`
|
|
560
574
|
);
|
|
561
575
|
} else {
|
|
562
576
|
counts.files++;
|
|
@@ -846,6 +860,7 @@ var CliFindCommandProcessor = class {
|
|
|
846
860
|
const resolvedBase = fs.resolvePath(searchPath);
|
|
847
861
|
const nameRegex = namePattern ? this.globToRegex(namePattern) : null;
|
|
848
862
|
const results = [];
|
|
863
|
+
this.fs = fs;
|
|
849
864
|
this.walk(
|
|
850
865
|
node,
|
|
851
866
|
resolvedBase,
|
|
@@ -873,8 +888,9 @@ var CliFindCommandProcessor = class {
|
|
|
873
888
|
return new RegExp(`^${regexStr}$`, "i");
|
|
874
889
|
}
|
|
875
890
|
walk(node, currentPath, depth, maxDepth, nameRegex, typeFilter, results) {
|
|
876
|
-
|
|
877
|
-
|
|
891
|
+
const children = this.fs ? readableChildren(this.fs, currentPath) : node.children;
|
|
892
|
+
if (!children) return;
|
|
893
|
+
for (const child of children) {
|
|
878
894
|
const childPath = currentPath === "/" ? `/${child.name}` : `${currentPath}/${child.name}`;
|
|
879
895
|
let matches = true;
|
|
880
896
|
if (nameRegex && !nameRegex.test(child.name)) {
|
|
@@ -1105,8 +1121,9 @@ var CliGrepCommandProcessor = class {
|
|
|
1105
1121
|
};
|
|
1106
1122
|
}
|
|
1107
1123
|
collectFiles(fs, basePath, node, result) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1124
|
+
const children = readableChildren(fs, basePath);
|
|
1125
|
+
if (!children) return;
|
|
1126
|
+
for (const child of children) {
|
|
1110
1127
|
const childPath = basePath === "/" ? `/${child.name}` : `${basePath}/${child.name}`;
|
|
1111
1128
|
if (child.type === "file") {
|
|
1112
1129
|
result.push(childPath);
|
|
@@ -2065,6 +2082,10 @@ var CliStatCommandProcessor = class {
|
|
|
2065
2082
|
}
|
|
2066
2083
|
for (const path of paths) {
|
|
2067
2084
|
const resolved = fs.resolvePath(path);
|
|
2085
|
+
if (fs.canTraverse && !fs.canTraverse(resolved)) {
|
|
2086
|
+
context.writer.writeError(`stat: cannot stat '${path}': Permission denied`);
|
|
2087
|
+
continue;
|
|
2088
|
+
}
|
|
2068
2089
|
const node = fs.getNode(resolved);
|
|
2069
2090
|
if (!node) {
|
|
2070
2091
|
context.writer.writeError(
|
|
@@ -2333,6 +2354,10 @@ var CliDuCommandProcessor = class {
|
|
|
2333
2354
|
}
|
|
2334
2355
|
for (const path of paths) {
|
|
2335
2356
|
const resolved = fs.resolvePath(path);
|
|
2357
|
+
if (fs.canTraverse && !fs.canTraverse(resolved)) {
|
|
2358
|
+
context.writer.writeError(`du: cannot access '${path}': Permission denied`);
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2336
2361
|
const node = fs.getNode(resolved);
|
|
2337
2362
|
if (!node) {
|
|
2338
2363
|
context.writer.writeError(
|
|
@@ -2346,6 +2371,7 @@ var CliDuCommandProcessor = class {
|
|
|
2346
2371
|
);
|
|
2347
2372
|
} else {
|
|
2348
2373
|
const entries = [];
|
|
2374
|
+
this.fs = fs;
|
|
2349
2375
|
this.collectSizes(node, resolved, 0, maxDepth, summaryOnly, entries);
|
|
2350
2376
|
for (const entry of entries) {
|
|
2351
2377
|
context.writer.writeln(
|
|
@@ -2368,8 +2394,9 @@ var CliDuCommandProcessor = class {
|
|
|
2368
2394
|
return node.size;
|
|
2369
2395
|
}
|
|
2370
2396
|
let total = 0;
|
|
2371
|
-
|
|
2372
|
-
|
|
2397
|
+
const children = this.fs ? readableChildren(this.fs, path) : node.children;
|
|
2398
|
+
if (children) {
|
|
2399
|
+
for (const child of children) {
|
|
2373
2400
|
const childPath = path === "/" ? `/${child.name}` : `${path}/${child.name}`;
|
|
2374
2401
|
total += this.collectSizes(
|
|
2375
2402
|
child,
|
|
@@ -3832,14 +3859,14 @@ function substituteVariables(input, variables) {
|
|
|
3832
3859
|
);
|
|
3833
3860
|
return result;
|
|
3834
3861
|
}
|
|
3835
|
-
|
|
3836
|
-
// src/lib/services/indexed-db-file-system.service.ts
|
|
3837
3862
|
var DB_NAME = "qodalis-cli-filesystem";
|
|
3838
3863
|
var STORE_NAME = "filesystem";
|
|
3839
3864
|
var ROOT_KEY = "root";
|
|
3840
3865
|
var CWD_KEY = "cwd";
|
|
3841
3866
|
var HOME_KEY = "home";
|
|
3842
3867
|
var DEFAULT_HOME = "/";
|
|
3868
|
+
var SYSTEM_OWNERSHIP = { uid: "root", gid: "admin" };
|
|
3869
|
+
var ANONYMOUS_OWNERSHIP = { uid: "nobody", gid: "nogroup" };
|
|
3843
3870
|
function createSeedFileSystem() {
|
|
3844
3871
|
const now = Date.now();
|
|
3845
3872
|
return {
|
|
@@ -3887,6 +3914,88 @@ var IndexedDbFileSystemService = class {
|
|
|
3887
3914
|
this.homePath = DEFAULT_HOME;
|
|
3888
3915
|
this.currentUid = null;
|
|
3889
3916
|
this.currentGroups = [];
|
|
3917
|
+
this.systemDepth = 0;
|
|
3918
|
+
}
|
|
3919
|
+
// --- Access control ---
|
|
3920
|
+
setAccessPolicy(policy) {
|
|
3921
|
+
this.accessPolicy = policy;
|
|
3922
|
+
}
|
|
3923
|
+
runAsSystem(fn) {
|
|
3924
|
+
this.systemDepth++;
|
|
3925
|
+
try {
|
|
3926
|
+
return fn();
|
|
3927
|
+
} finally {
|
|
3928
|
+
this.systemDepth--;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
reassignOwner(fromUid, toUid) {
|
|
3932
|
+
const from = fromUid.toLowerCase();
|
|
3933
|
+
if (!from || !toUid) return 0;
|
|
3934
|
+
let changed = 0;
|
|
3935
|
+
const visit = (node) => {
|
|
3936
|
+
if (node.ownership && node.ownership.uid.toLowerCase() === from) {
|
|
3937
|
+
node.ownership = { ...node.ownership, uid: toUid };
|
|
3938
|
+
changed++;
|
|
3939
|
+
}
|
|
3940
|
+
node.children?.forEach(visit);
|
|
3941
|
+
};
|
|
3942
|
+
visit(this.root);
|
|
3943
|
+
return changed;
|
|
3944
|
+
}
|
|
3945
|
+
canTraverse(path) {
|
|
3946
|
+
try {
|
|
3947
|
+
this.assertTraverse(this.resolvePath(path), "stat", path);
|
|
3948
|
+
return true;
|
|
3949
|
+
} catch {
|
|
3950
|
+
return false;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
get enforcing() {
|
|
3954
|
+
return !!this.accessPolicy && this.systemDepth === 0;
|
|
3955
|
+
}
|
|
3956
|
+
deny(op, path, reason = "Permission denied") {
|
|
3957
|
+
throw new cliCore.CliAccessDeniedError(`${op}: ${path}: ${reason}`);
|
|
3958
|
+
}
|
|
3959
|
+
/** `x` on every existing directory on the way to `resolved` (excluding the target). */
|
|
3960
|
+
assertTraverse(resolved, op, display) {
|
|
3961
|
+
if (!this.enforcing) return;
|
|
3962
|
+
const parts = resolved.split("/").filter(Boolean);
|
|
3963
|
+
let current = this.root;
|
|
3964
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3965
|
+
if (!current || current.type !== "directory") return;
|
|
3966
|
+
if (!this.accessPolicy.canAccess(current, "execute")) this.deny(op, display);
|
|
3967
|
+
current = current.children?.find((c) => c.name === parts[i]);
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
assertAccess(node, action, op, display) {
|
|
3971
|
+
if (this.enforcing && !this.accessPolicy.canAccess(node, action)) this.deny(op, display);
|
|
3972
|
+
}
|
|
3973
|
+
/** Recursive delete: every directory inside must allow removing its entries (`w` + `x`, readable). */
|
|
3974
|
+
assertRemovableTree(dir, dirPath, display) {
|
|
3975
|
+
if (!this.enforcing) return;
|
|
3976
|
+
this.assertAccess(dir, "read", "rm", display);
|
|
3977
|
+
this.assertAccess(dir, "execute", "rm", display);
|
|
3978
|
+
if ((dir.children?.length ?? 0) > 0) this.assertAccess(dir, "write", "rm", display);
|
|
3979
|
+
for (const child of dir.children ?? []) {
|
|
3980
|
+
if (child.type === "directory") this.assertRemovableTree(child, `${dirPath}/${child.name}`, display);
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
/** Copying reads everything it copies: `r` on every file, `r` + `x` on every directory. */
|
|
3984
|
+
assertReadableTree(node, display) {
|
|
3985
|
+
if (!this.enforcing) return;
|
|
3986
|
+
this.assertAccess(node, "read", "cp", display);
|
|
3987
|
+
if (node.type !== "directory") return;
|
|
3988
|
+
this.assertAccess(node, "execute", "cp", display);
|
|
3989
|
+
for (const child of node.children ?? []) {
|
|
3990
|
+
this.assertReadableTree(child, `${display.replace(/\/+$/, "")}/${child.name}`);
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
/** Creating/removing entries needs `w` and `x` on the containing directory. */
|
|
3994
|
+
assertModifyDir(dir, dirPath, op, display) {
|
|
3995
|
+
if (!this.enforcing) return;
|
|
3996
|
+
this.assertTraverse(dirPath, op, display);
|
|
3997
|
+
this.assertAccess(dir, "execute", op, display);
|
|
3998
|
+
this.assertAccess(dir, "write", op, display);
|
|
3890
3999
|
}
|
|
3891
4000
|
// --- User context ---
|
|
3892
4001
|
setCurrentUser(uid, groups) {
|
|
@@ -3894,7 +4003,9 @@ var IndexedDbFileSystemService = class {
|
|
|
3894
4003
|
this.currentGroups = groups;
|
|
3895
4004
|
}
|
|
3896
4005
|
getDefaultOwnership() {
|
|
3897
|
-
if (!this.currentUid)
|
|
4006
|
+
if (!this.currentUid) {
|
|
4007
|
+
return this.accessPolicy ? { ...ANONYMOUS_OWNERSHIP } : void 0;
|
|
4008
|
+
}
|
|
3898
4009
|
return {
|
|
3899
4010
|
uid: this.currentUid,
|
|
3900
4011
|
gid: this.currentGroups[0] || "users"
|
|
@@ -3913,6 +4024,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3913
4024
|
if (node.type !== "directory") {
|
|
3914
4025
|
throw new Error(`cd: ${path}: Not a directory`);
|
|
3915
4026
|
}
|
|
4027
|
+
this.assertTraverse(resolved, "cd", path);
|
|
4028
|
+
this.assertAccess(node, "execute", "cd", path);
|
|
3916
4029
|
this.cwd = resolved;
|
|
3917
4030
|
}
|
|
3918
4031
|
getHomePath() {
|
|
@@ -3969,6 +4082,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3969
4082
|
if (node.type !== "directory") {
|
|
3970
4083
|
throw new Error(`ls: ${path}: Not a directory`);
|
|
3971
4084
|
}
|
|
4085
|
+
this.assertTraverse(this.resolvePath(path), "ls", path);
|
|
4086
|
+
this.assertAccess(node, "read", "ls", path);
|
|
3972
4087
|
return node.children || [];
|
|
3973
4088
|
}
|
|
3974
4089
|
readFile(path) {
|
|
@@ -3979,6 +4094,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3979
4094
|
if (node.type === "directory") {
|
|
3980
4095
|
throw new Error(`cat: ${path}: Is a directory`);
|
|
3981
4096
|
}
|
|
4097
|
+
this.assertTraverse(this.resolvePath(path), "cat", path);
|
|
4098
|
+
this.assertAccess(node, "read", "cat", path);
|
|
3982
4099
|
return node.content ?? "";
|
|
3983
4100
|
}
|
|
3984
4101
|
exists(path) {
|
|
@@ -4003,6 +4120,10 @@ var IndexedDbFileSystemService = class {
|
|
|
4003
4120
|
}
|
|
4004
4121
|
let child = current.children.find((c) => c.name === part);
|
|
4005
4122
|
if (!child) {
|
|
4123
|
+
if (this.enforcing) {
|
|
4124
|
+
this.assertAccess(current, "execute", "mkdir", path);
|
|
4125
|
+
this.assertAccess(current, "write", "mkdir", path);
|
|
4126
|
+
}
|
|
4006
4127
|
const now = Date.now();
|
|
4007
4128
|
child = {
|
|
4008
4129
|
name: part,
|
|
@@ -4018,6 +4139,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4018
4139
|
current.modifiedAt = now;
|
|
4019
4140
|
} else if (child.type !== "directory") {
|
|
4020
4141
|
throw new Error(`mkdir: ${part}: Not a directory`);
|
|
4142
|
+
} else {
|
|
4143
|
+
this.assertAccess(child, "execute", "mkdir", path);
|
|
4021
4144
|
}
|
|
4022
4145
|
current = child;
|
|
4023
4146
|
}
|
|
@@ -4036,6 +4159,7 @@ var IndexedDbFileSystemService = class {
|
|
|
4036
4159
|
if (parent.children?.find((c) => c.name === dirName)) {
|
|
4037
4160
|
throw new Error(`mkdir: ${path}: File exists`);
|
|
4038
4161
|
}
|
|
4162
|
+
this.assertModifyDir(parent, parentPath, "mkdir", path);
|
|
4039
4163
|
const now = Date.now();
|
|
4040
4164
|
if (!parent.children) {
|
|
4041
4165
|
parent.children = [];
|
|
@@ -4070,6 +4194,12 @@ var IndexedDbFileSystemService = class {
|
|
|
4070
4194
|
}
|
|
4071
4195
|
const existing = parent.children?.find((c) => c.name === fileName);
|
|
4072
4196
|
const now = Date.now();
|
|
4197
|
+
if (existing) {
|
|
4198
|
+
this.assertTraverse(resolved, "touch", path);
|
|
4199
|
+
this.assertAccess(existing, "write", "touch", path);
|
|
4200
|
+
} else {
|
|
4201
|
+
this.assertModifyDir(parent, parentPath, "touch", path);
|
|
4202
|
+
}
|
|
4073
4203
|
if (existing) {
|
|
4074
4204
|
existing.modifiedAt = now;
|
|
4075
4205
|
} else {
|
|
@@ -4096,6 +4226,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4096
4226
|
if (node.type === "directory") {
|
|
4097
4227
|
throw new Error(`write: ${path}: Is a directory`);
|
|
4098
4228
|
}
|
|
4229
|
+
this.assertTraverse(resolved, "write", path);
|
|
4230
|
+
this.assertAccess(node, "write", "write", path);
|
|
4099
4231
|
const now = Date.now();
|
|
4100
4232
|
node.content = append ? (node.content ?? "") + content : content;
|
|
4101
4233
|
node.size = new Blob([node.content]).size;
|
|
@@ -4124,6 +4256,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4124
4256
|
if (target.type === "directory" && !recursive) {
|
|
4125
4257
|
throw new Error(`rm: ${path}: Is a directory`);
|
|
4126
4258
|
}
|
|
4259
|
+
this.assertModifyDir(parent, parentPath, "rm", path);
|
|
4260
|
+
if (target.type === "directory") this.assertRemovableTree(target, resolved, path);
|
|
4127
4261
|
parent.children.splice(index, 1);
|
|
4128
4262
|
parent.modifiedAt = Date.now();
|
|
4129
4263
|
}
|
|
@@ -4138,10 +4272,13 @@ var IndexedDbFileSystemService = class {
|
|
|
4138
4272
|
`cp: -r not specified; omitting directory '${src}'`
|
|
4139
4273
|
);
|
|
4140
4274
|
}
|
|
4275
|
+
this.assertTraverse(srcResolved, "cp", src);
|
|
4276
|
+
this.assertReadableTree(srcNode, src);
|
|
4141
4277
|
const destResolved = this.resolvePath(dest);
|
|
4142
4278
|
const destNode = this.getNode(destResolved);
|
|
4143
4279
|
if (destNode && destNode.type === "directory") {
|
|
4144
|
-
|
|
4280
|
+
this.assertModifyDir(destNode, destResolved, "cp", dest);
|
|
4281
|
+
const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
|
|
4145
4282
|
if (!destNode.children) {
|
|
4146
4283
|
destNode.children = [];
|
|
4147
4284
|
}
|
|
@@ -4161,7 +4298,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4161
4298
|
if (!parent || parent.type !== "directory") {
|
|
4162
4299
|
throw new Error(`cp: ${parentPath}: No such file or directory`);
|
|
4163
4300
|
}
|
|
4164
|
-
|
|
4301
|
+
this.assertModifyDir(parent, parentPath, "cp", dest);
|
|
4302
|
+
const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
|
|
4165
4303
|
clone.name = newName;
|
|
4166
4304
|
if (!parent.children) {
|
|
4167
4305
|
parent.children = [];
|
|
@@ -4193,10 +4331,12 @@ var IndexedDbFileSystemService = class {
|
|
|
4193
4331
|
if (!srcParent || !srcParent.children) {
|
|
4194
4332
|
throw new Error(`mv: ${src}: No such file or directory`);
|
|
4195
4333
|
}
|
|
4334
|
+
this.assertModifyDir(srcParent, srcParentPath, "mv", src);
|
|
4196
4335
|
const srcIndex = srcParent.children.findIndex(
|
|
4197
4336
|
(c) => c.name === srcNode.name
|
|
4198
4337
|
);
|
|
4199
4338
|
if (destNode && destNode.type === "directory") {
|
|
4339
|
+
this.assertModifyDir(destNode, destResolved, "mv", dest);
|
|
4200
4340
|
if (!destNode.children) {
|
|
4201
4341
|
destNode.children = [];
|
|
4202
4342
|
}
|
|
@@ -4219,6 +4359,7 @@ var IndexedDbFileSystemService = class {
|
|
|
4219
4359
|
`mv: ${destParentPath}: No such file or directory`
|
|
4220
4360
|
);
|
|
4221
4361
|
}
|
|
4362
|
+
this.assertModifyDir(destParent, destParentPath, "mv", dest);
|
|
4222
4363
|
srcParent.children.splice(srcIndex, 1);
|
|
4223
4364
|
srcNode.name = newName;
|
|
4224
4365
|
if (!destParent.children) {
|
|
@@ -4242,6 +4383,9 @@ var IndexedDbFileSystemService = class {
|
|
|
4242
4383
|
if (!node) {
|
|
4243
4384
|
throw new Error(`chmod: ${path}: No such file or directory`);
|
|
4244
4385
|
}
|
|
4386
|
+
if (this.enforcing && !this.accessPolicy.canChangeOwnership(node)) {
|
|
4387
|
+
this.deny("chmod", path, "Operation not permitted");
|
|
4388
|
+
}
|
|
4245
4389
|
node.permissions = permissions;
|
|
4246
4390
|
}
|
|
4247
4391
|
chown(path, ownership) {
|
|
@@ -4250,6 +4394,9 @@ var IndexedDbFileSystemService = class {
|
|
|
4250
4394
|
if (!node) {
|
|
4251
4395
|
throw new Error(`chown: ${path}: No such file or directory`);
|
|
4252
4396
|
}
|
|
4397
|
+
if (this.enforcing && !this.accessPolicy.canChangeOwnership(node, ownership)) {
|
|
4398
|
+
this.deny("chown", path, "Operation not permitted");
|
|
4399
|
+
}
|
|
4253
4400
|
node.ownership = ownership;
|
|
4254
4401
|
}
|
|
4255
4402
|
// --- Persistence ---
|
|
@@ -4291,6 +4438,18 @@ var IndexedDbFileSystemService = class {
|
|
|
4291
4438
|
}
|
|
4292
4439
|
}
|
|
4293
4440
|
// --- Private helpers ---
|
|
4441
|
+
/** A copy belongs to whoever made it (like `cp` without `-p`) */
|
|
4442
|
+
ownedByCurrentUser(node) {
|
|
4443
|
+
const owner = this.getDefaultOwnership();
|
|
4444
|
+
if (owner) {
|
|
4445
|
+
const apply = (n) => {
|
|
4446
|
+
n.ownership = { ...owner };
|
|
4447
|
+
n.children?.forEach(apply);
|
|
4448
|
+
};
|
|
4449
|
+
apply(node);
|
|
4450
|
+
}
|
|
4451
|
+
return node;
|
|
4452
|
+
}
|
|
4294
4453
|
cloneNode(node) {
|
|
4295
4454
|
const now = Date.now();
|
|
4296
4455
|
const clone = {
|
|
@@ -4965,6 +5124,115 @@ var VirtualFsFileTransferService = class {
|
|
|
4965
5124
|
return this._browserService.uploadFromBrowser(accept);
|
|
4966
5125
|
}
|
|
4967
5126
|
};
|
|
5127
|
+
var CliFilesPermissions = {
|
|
5128
|
+
/** Bypass all file permission checks */
|
|
5129
|
+
Admin: "files:admin",
|
|
5130
|
+
/** chmod/chown files you do not own */
|
|
5131
|
+
Chown: "files:chown"
|
|
5132
|
+
};
|
|
5133
|
+
var HOME_DIR_PERMISSIONS = "rwxr-x---";
|
|
5134
|
+
function authorization(context) {
|
|
5135
|
+
try {
|
|
5136
|
+
return context.services.get(cliCore.ICliAuthorizationService_TOKEN);
|
|
5137
|
+
} catch {
|
|
5138
|
+
return void 0;
|
|
5139
|
+
}
|
|
5140
|
+
}
|
|
5141
|
+
function createFileAccessPolicy(context) {
|
|
5142
|
+
return {
|
|
5143
|
+
// Nodes without an owner (system seed, legacy data) belong to root:admin
|
|
5144
|
+
canAccess: (node, action) => cliCore.cliCanAccess(
|
|
5145
|
+
context,
|
|
5146
|
+
node.ownership ? node : { ...node, ownership: SYSTEM_OWNERSHIP },
|
|
5147
|
+
action,
|
|
5148
|
+
[CliFilesPermissions.Admin]
|
|
5149
|
+
),
|
|
5150
|
+
canChangeOwnership: (node, newOwnership) => {
|
|
5151
|
+
const auth = authorization(context);
|
|
5152
|
+
if (!auth) return true;
|
|
5153
|
+
if (auth.can(CliFilesPermissions.Admin) || auth.can(CliFilesPermissions.Chown)) return true;
|
|
5154
|
+
const principal = auth.getPrincipal();
|
|
5155
|
+
if (!principal) return false;
|
|
5156
|
+
const owns = !!node.ownership && (node.ownership.uid === principal.name || node.ownership.uid === principal.id);
|
|
5157
|
+
const keepsOwner = !newOwnership || newOwnership.uid === node.ownership?.uid || newOwnership.uid === principal.name;
|
|
5158
|
+
return owns && keepsOwner;
|
|
5159
|
+
}
|
|
5160
|
+
};
|
|
5161
|
+
}
|
|
5162
|
+
function followUserSession(context, fs) {
|
|
5163
|
+
let sessions;
|
|
5164
|
+
try {
|
|
5165
|
+
sessions = context.services.get(cliCore.ICliUserSessionService_TOKEN);
|
|
5166
|
+
} catch {
|
|
5167
|
+
sessions = void 0;
|
|
5168
|
+
}
|
|
5169
|
+
if (!sessions) return void 0;
|
|
5170
|
+
let previousUser = null;
|
|
5171
|
+
return sessions.getUserSession().subscribe((session) => {
|
|
5172
|
+
if (!session?.user) {
|
|
5173
|
+
fs.setCurrentUser("", []);
|
|
5174
|
+
if (previousUser !== null || !canEnter(fs, fs.getCurrentDirectory())) {
|
|
5175
|
+
try {
|
|
5176
|
+
fs.setCurrentDirectory("/");
|
|
5177
|
+
void fs.persist();
|
|
5178
|
+
} catch {
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
previousUser = null;
|
|
5182
|
+
return;
|
|
5183
|
+
}
|
|
5184
|
+
const user = session.user;
|
|
5185
|
+
fs.setCurrentUser(user.name, user.groups ?? []);
|
|
5186
|
+
if (user.homeDir) {
|
|
5187
|
+
const home = user.homeDir;
|
|
5188
|
+
const system = (fn) => fs.runAsSystem ? fs.runAsSystem(fn) : fn();
|
|
5189
|
+
if (!fs.exists(home)) {
|
|
5190
|
+
system(() => {
|
|
5191
|
+
fs.createDirectory(home, true);
|
|
5192
|
+
if (home !== "/") {
|
|
5193
|
+
fs.chown(home, { uid: user.name, gid: user.groups?.[0] ?? "users" });
|
|
5194
|
+
fs.chmod(home, HOME_DIR_PERMISSIONS);
|
|
5195
|
+
}
|
|
5196
|
+
});
|
|
5197
|
+
void fs.persist();
|
|
5198
|
+
}
|
|
5199
|
+
fs.setHomePath(home);
|
|
5200
|
+
if (previousUser !== user.id) {
|
|
5201
|
+
try {
|
|
5202
|
+
fs.setCurrentDirectory(home);
|
|
5203
|
+
} catch {
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
}
|
|
5207
|
+
previousUser = user.id;
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
function canEnter(fs, path) {
|
|
5211
|
+
try {
|
|
5212
|
+
fs.setCurrentDirectory(path);
|
|
5213
|
+
return true;
|
|
5214
|
+
} catch {
|
|
5215
|
+
return false;
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
function followUserAccounts(context, fs) {
|
|
5219
|
+
let events;
|
|
5220
|
+
try {
|
|
5221
|
+
events = context.services.get(cliCore.ICliUserEventService_TOKEN);
|
|
5222
|
+
} catch {
|
|
5223
|
+
events = void 0;
|
|
5224
|
+
}
|
|
5225
|
+
if (!events || !fs.reassignOwner) return void 0;
|
|
5226
|
+
return events.events$.subscribe((event) => {
|
|
5227
|
+
let changed = 0;
|
|
5228
|
+
if (event.type === "user_updated" && event.previousName && event.previousName !== event.user?.name) {
|
|
5229
|
+
changed = fs.reassignOwner(event.previousName, event.user.name);
|
|
5230
|
+
} else if (event.type === "user_deleted" && event.user?.name) {
|
|
5231
|
+
changed = fs.reassignOwner(event.user.name, SYSTEM_OWNERSHIP.uid);
|
|
5232
|
+
}
|
|
5233
|
+
if (changed > 0) void fs.persist().catch(() => void 0);
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
4968
5236
|
|
|
4969
5237
|
// src/lib/completion/file-path-completion-provider.ts
|
|
4970
5238
|
var FILE_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -5159,6 +5427,10 @@ var filesModule = {
|
|
|
5159
5427
|
new CliShCommandProcessor(),
|
|
5160
5428
|
new CliUploadCommandProcessor()
|
|
5161
5429
|
],
|
|
5430
|
+
permissions: [
|
|
5431
|
+
{ name: CliFilesPermissions.Admin, description: "Bypass file permission checks" },
|
|
5432
|
+
{ name: CliFilesPermissions.Chown, description: "chmod/chown files owned by others" }
|
|
5433
|
+
],
|
|
5162
5434
|
services: [
|
|
5163
5435
|
{
|
|
5164
5436
|
provide: IFileSystemService_TOKEN,
|
|
@@ -5230,13 +5502,26 @@ var filesModule = {
|
|
|
5230
5502
|
});
|
|
5231
5503
|
}
|
|
5232
5504
|
},
|
|
5505
|
+
async onAfterBoot(context) {
|
|
5506
|
+
const fs = context.services.getRequired(IFileSystemService_TOKEN);
|
|
5507
|
+
fs.setAccessPolicy?.(createFileAccessPolicy(context));
|
|
5508
|
+
this._sessionSubscription?.unsubscribe();
|
|
5509
|
+
this._sessionSubscription = followUserSession(context, fs);
|
|
5510
|
+
this._accountsSubscription?.unsubscribe();
|
|
5511
|
+
this._accountsSubscription = followUserAccounts(context, fs);
|
|
5512
|
+
},
|
|
5233
5513
|
async onDestroy(_context) {
|
|
5234
5514
|
const sub = this._dragDropSubscription;
|
|
5235
5515
|
sub?.unsubscribe();
|
|
5236
5516
|
this._dragDropSubscription = void 0;
|
|
5517
|
+
this._sessionSubscription?.unsubscribe();
|
|
5518
|
+
this._sessionSubscription = void 0;
|
|
5519
|
+
this._accountsSubscription?.unsubscribe();
|
|
5520
|
+
this._accountsSubscription = void 0;
|
|
5237
5521
|
}
|
|
5238
5522
|
};
|
|
5239
5523
|
|
|
5524
|
+
exports.ANONYMOUS_OWNERSHIP = ANONYMOUS_OWNERSHIP;
|
|
5240
5525
|
exports.CliAwkCommandProcessor = CliAwkCommandProcessor;
|
|
5241
5526
|
exports.CliBasenameCommandProcessor = CliBasenameCommandProcessor;
|
|
5242
5527
|
exports.CliCatCommandProcessor = CliCatCommandProcessor;
|
|
@@ -5249,6 +5534,7 @@ exports.CliDiffCommandProcessor = CliDiffCommandProcessor;
|
|
|
5249
5534
|
exports.CliDirnameCommandProcessor = CliDirnameCommandProcessor;
|
|
5250
5535
|
exports.CliDuCommandProcessor = CliDuCommandProcessor;
|
|
5251
5536
|
exports.CliEchoCommandProcessor = CliEchoCommandProcessor;
|
|
5537
|
+
exports.CliFilesPermissions = CliFilesPermissions;
|
|
5252
5538
|
exports.CliFindCommandProcessor = CliFindCommandProcessor;
|
|
5253
5539
|
exports.CliGrepCommandProcessor = CliGrepCommandProcessor;
|
|
5254
5540
|
exports.CliHeadCommandProcessor = CliHeadCommandProcessor;
|
|
@@ -5274,11 +5560,16 @@ exports.CliUniqCommandProcessor = CliUniqCommandProcessor;
|
|
|
5274
5560
|
exports.CliWcCommandProcessor = CliWcCommandProcessor;
|
|
5275
5561
|
exports.CliXargsCommandProcessor = CliXargsCommandProcessor;
|
|
5276
5562
|
exports.FilePathCompletionProvider = FilePathCompletionProvider;
|
|
5563
|
+
exports.HOME_DIR_PERMISSIONS = HOME_DIR_PERMISSIONS;
|
|
5277
5564
|
exports.IFileSystemService_TOKEN = IFileSystemService_TOKEN;
|
|
5278
5565
|
exports.IndexedDbFileSystemService = IndexedDbFileSystemService;
|
|
5566
|
+
exports.SYSTEM_OWNERSHIP = SYSTEM_OWNERSHIP;
|
|
5279
5567
|
exports.ServerFileSystemService = ServerFileSystemService;
|
|
5280
5568
|
exports.VirtualFsFileTransferService = VirtualFsFileTransferService;
|
|
5569
|
+
exports.createFileAccessPolicy = createFileAccessPolicy;
|
|
5281
5570
|
exports.executeScript = executeScript;
|
|
5282
5571
|
exports.filesModule = filesModule;
|
|
5572
|
+
exports.followUserAccounts = followUserAccounts;
|
|
5573
|
+
exports.followUserSession = followUserSession;
|
|
5283
5574
|
//# sourceMappingURL=public-api.js.map
|
|
5284
5575
|
//# sourceMappingURL=public-api.js.map
|