u
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getStore } from '../db/store.js';
|
||||
import { requireSelectedCompanyAdmin } from '../db/companyuser.js';
|
||||
import { getDefaultSkills } from './skill.js';
|
||||
import { getDefaultApplets, DEFAULT_APPLETS_VERSION } from './applet.js';
|
||||
|
||||
@@ -97,6 +98,39 @@ export function isReadOnlyDatabasePath(filePath) {
|
||||
return lower === 'image' || lower.startsWith('image/') || Boolean(extractFullHash(clean));
|
||||
}
|
||||
|
||||
export function isProtectedAdminVfsPath(filePath) {
|
||||
if (typeof filePath !== 'string' || !filePath.trim()) return false;
|
||||
const clean = cleanVirtualPath(filePath, true);
|
||||
const lower = clean.toLowerCase();
|
||||
return (
|
||||
lower.endsWith('.skill') ||
|
||||
lower.endsWith('.applet.js') ||
|
||||
lower.endsWith('.apple.js') ||
|
||||
lower === 'apple.js' ||
|
||||
lower.endsWith('/apple.js')
|
||||
);
|
||||
}
|
||||
|
||||
export function directoryContainsProtectedAdminVfsFile(absDirPath) {
|
||||
try {
|
||||
if (!fs.existsSync(absDirPath)) return false;
|
||||
const stat = fs.statSync(absDirPath);
|
||||
if (!stat.isDirectory()) return isProtectedAdminVfsPath(absDirPath);
|
||||
const entries = fs.readdirSync(absDirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(absDirPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (directoryContainsProtectedAdminVfsFile(fullPath)) return true;
|
||||
} else if (isProtectedAdminVfsPath(entry.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function assertNotReadOnlyDatabasePath(filePath) {
|
||||
if (isReadOnlyDatabasePath(filePath)) {
|
||||
throw new Error(
|
||||
@@ -190,6 +224,9 @@ export async function loadDefaultSkills(ctx = {}) {
|
||||
* @returns {Promise<Array<string>>}
|
||||
*/
|
||||
export async function restoreDefaultSkills(ctx = {}) {
|
||||
if (ctx.googleAuthId != null) {
|
||||
await requireSelectedCompanyAdmin(ctx.googleAuthId);
|
||||
}
|
||||
return seedVfsFiles(ctx, getDefaultSkills(), { overwrite: true, label: 'skill' });
|
||||
}
|
||||
|
||||
@@ -208,6 +245,9 @@ export async function loadDefaultApplets(ctx = {}) {
|
||||
* @returns {Promise<Array<string>>}
|
||||
*/
|
||||
export async function restoreDefaultApplets(ctx = {}) {
|
||||
if (ctx.googleAuthId != null) {
|
||||
await requireSelectedCompanyAdmin(ctx.googleAuthId);
|
||||
}
|
||||
return seedVfsFiles(ctx, getVersionedApplets(), { overwrite: true, label: 'applet' });
|
||||
}
|
||||
|
||||
@@ -293,7 +333,14 @@ function cbWrap(syncFn, arityWithOpt = 3) {
|
||||
* @param {{ googleAuthId: number }} [options]
|
||||
*/
|
||||
export function createVfs(vfsRoot, options = {}) {
|
||||
const { googleAuthId } = options;
|
||||
const { googleAuthId, isAdmin = true } = options;
|
||||
|
||||
const assertWritablePath = (filePath) => {
|
||||
assertNotReadOnlyDatabasePath(filePath);
|
||||
if (!isAdmin && isProtectedAdminVfsPath(filePath)) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
};
|
||||
|
||||
// #region Internal Sandbox Helpers
|
||||
// MARK: - Internal Sandbox Helpers
|
||||
@@ -405,7 +452,7 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
|
||||
// --- Write & Append Operations ---
|
||||
writeFileSync: (p, data, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
|
||||
try {
|
||||
@@ -416,14 +463,14 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
|
||||
createWriteStream: (p, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
|
||||
return fs.createWriteStream(diskPath, opt);
|
||||
},
|
||||
|
||||
appendFileSync: (p, data, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
fs.mkdirSync(path.dirname(diskPath), { recursive: true });
|
||||
try {
|
||||
@@ -435,7 +482,7 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
|
||||
// --- Copy, Rename & Truncate Operations ---
|
||||
copyFileSync: (src, dest, mode) => {
|
||||
assertNotReadOnlyDatabasePath(dest);
|
||||
assertWritablePath(dest);
|
||||
const hash = extractFullHash(src);
|
||||
if (hash) {
|
||||
const buf = syncFetchImage(hash);
|
||||
@@ -452,7 +499,7 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
|
||||
cpSync: (src, dest, opt) => {
|
||||
assertNotReadOnlyDatabasePath(dest);
|
||||
assertWritablePath(dest);
|
||||
const srcDisk = safeResolve(src);
|
||||
const destDisk = safeResolve(dest);
|
||||
fs.mkdirSync(path.dirname(destDisk), { recursive: true });
|
||||
@@ -464,10 +511,13 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
|
||||
renameSync: (oldPath, newPath) => {
|
||||
assertNotReadOnlyDatabasePath(oldPath);
|
||||
assertNotReadOnlyDatabasePath(newPath);
|
||||
assertWritablePath(oldPath);
|
||||
assertWritablePath(newPath);
|
||||
const oldDisk = safeResolve(oldPath);
|
||||
const newDisk = safeResolve(newPath);
|
||||
if (!isAdmin && (directoryContainsProtectedAdminVfsFile(oldDisk) || directoryContainsProtectedAdminVfsFile(newDisk))) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
fs.mkdirSync(path.dirname(newDisk), { recursive: true });
|
||||
try {
|
||||
return fs.renameSync(oldDisk, newDisk);
|
||||
@@ -477,7 +527,7 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
|
||||
truncateSync: (p, len) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
try {
|
||||
return fs.truncateSync(diskPath, len);
|
||||
@@ -600,7 +650,7 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
|
||||
unlinkSync: (p) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
try {
|
||||
return fs.unlinkSync(diskPath);
|
||||
@@ -610,8 +660,11 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
|
||||
rmdirSync: (p, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
try {
|
||||
return fs.rmdirSync(diskPath, opt);
|
||||
} catch (err) {
|
||||
@@ -620,8 +673,11 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
},
|
||||
|
||||
rmSync: (p, opt) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
const diskPath = safeResolve(p);
|
||||
if (!isAdmin && directoryContainsProtectedAdminVfsFile(diskPath)) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
try {
|
||||
return fs.rmSync(diskPath, opt);
|
||||
} catch (err) {
|
||||
@@ -662,7 +718,7 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
writeFile: async (p, data, opt) => vfs.writeFileSync(p, data, opt),
|
||||
appendFile: async (p, data, opt) => vfs.appendFileSync(p, data, opt),
|
||||
copyFile: async (src, dest, mode) => {
|
||||
assertNotReadOnlyDatabasePath(dest);
|
||||
assertWritablePath(dest);
|
||||
const hash = extractFullHash(src);
|
||||
if (hash) {
|
||||
const buf = await vfs.promises.readFile(src);
|
||||
@@ -695,7 +751,7 @@ export function createVfs(vfsRoot, options = {}) {
|
||||
* containment + read-only guards as `writeFileSync`.
|
||||
*/
|
||||
resolveWritePath: (p) => {
|
||||
assertNotReadOnlyDatabasePath(p);
|
||||
assertWritablePath(p);
|
||||
return safeResolve(p);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user