Improve security by isolating code execution more
This commit enhances application security against potential attacks by isolating dependencies that access the host system (like file operations) from the renderer process. It narrows the exposed functionality to script execution only, adding an extra security layer. The changes allow secure and scalable API exposure, preparing for future functionalities such as desktop notifications for script errors (#264), improved script execution handling (#296), and creating restore points (#50) in a secure and repeatable way. Changes include: - Inject `CodeRunner` into Vue components via dependency injection. - Move `CodeRunner` to the application layer as an abstraction for better domain-driven design alignment. - Refactor `SystemOperations` and related interfaces, removing the `I` prefix. - Update architecture documentation for clarity. - Update return types in `NodeSystemOperations` to match the Node APIs. - Improve `WindowVariablesProvider` integration tests for better error context. - Centralize type checks with common functions like `isArray` and `isNumber`. - Change `CodeRunner` to use `os` parameter, ensuring correct window variable injection. - Streamline API exposure to the renderer process: - Automatically bind function contexts to prevent loss of original context. - Implement a way to create facades (wrapper/proxy objects) for increased security.
This commit is contained in:
@@ -1,17 +1,16 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RuntimeEnvironmentStub } from '@tests/unit/shared/Stubs/RuntimeEnvironmentStub';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CodeRunner } from '@/infrastructure/CodeRunner';
|
||||
import { FileSystemOps, SystemOperations } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations';
|
||||
import { TemporaryFileCodeRunner } from '@/infrastructure/CodeRunner/TemporaryFileCodeRunner';
|
||||
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
|
||||
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
|
||||
import { OperatingSystemOpsStub } from '@tests/unit/shared/Stubs/OperatingSystemOpsStub';
|
||||
import { LocationOpsStub } from '@tests/unit/shared/Stubs/LocationOpsStub';
|
||||
import { FileSystemOpsStub } from '@tests/unit/shared/Stubs/FileSystemOpsStub';
|
||||
import { CommandOpsStub } from '@tests/unit/shared/Stubs/CommandOpsStub';
|
||||
import { IFileSystemOps, ISystemOperations } from '@/infrastructure/SystemOperations/ISystemOperations';
|
||||
import { FunctionKeys } from '@/TypeHelpers';
|
||||
|
||||
describe('CodeRunner', () => {
|
||||
describe('TemporaryFileCodeRunner', () => {
|
||||
describe('runCode', () => {
|
||||
it('creates temporary directory recursively', async () => {
|
||||
// arrange
|
||||
@@ -121,11 +120,11 @@ describe('CodeRunner', () => {
|
||||
describe('executes as expected', () => {
|
||||
// arrange
|
||||
const filePath = 'expected-file-path';
|
||||
interface IExecutionTestCase {
|
||||
interface ExecutionTestCase {
|
||||
readonly givenOs: OperatingSystem;
|
||||
readonly expectedCommand: string;
|
||||
}
|
||||
const testData: readonly IExecutionTestCase[] = [
|
||||
const testData: readonly ExecutionTestCase[] = [
|
||||
{
|
||||
givenOs: OperatingSystem.Windows,
|
||||
expectedCommand: filePath,
|
||||
@@ -164,7 +163,7 @@ describe('CodeRunner', () => {
|
||||
}
|
||||
});
|
||||
it('runs in expected order', async () => { // verifies correct `async`, `await` usage.
|
||||
const expectedOrder: readonly FunctionKeys<IFileSystemOps>[] = [
|
||||
const expectedOrder: readonly FunctionKeys<FileSystemOps>[] = [
|
||||
'createDirectory',
|
||||
'writeToFile',
|
||||
'setFilePermissions',
|
||||
@@ -186,7 +185,7 @@ describe('CodeRunner', () => {
|
||||
describe('throws with invalid OS', () => {
|
||||
const testScenarios: ReadonlyArray<{
|
||||
readonly description: string;
|
||||
readonly invalidOs: OperatingSystem | undefined;
|
||||
readonly invalidOs: OperatingSystem;
|
||||
readonly expectedError: string;
|
||||
}> = [
|
||||
(() => {
|
||||
@@ -197,11 +196,6 @@ describe('CodeRunner', () => {
|
||||
expectedError: `unsupported os: ${OperatingSystem[unsupportedOs]}`,
|
||||
};
|
||||
})(),
|
||||
{
|
||||
description: 'unknown OS',
|
||||
invalidOs: undefined,
|
||||
expectedError: 'Unidentified operating system',
|
||||
},
|
||||
];
|
||||
testScenarios.forEach(({ description, invalidOs, expectedError }) => {
|
||||
it(description, async () => {
|
||||
@@ -225,19 +219,17 @@ class TestContext {
|
||||
|
||||
private fileExtension = 'fileExtension';
|
||||
|
||||
private os: OperatingSystem | undefined = OperatingSystem.Windows;
|
||||
private os: OperatingSystem = OperatingSystem.Windows;
|
||||
|
||||
private systemOperations: ISystemOperations = new SystemOperationsStub();
|
||||
private systemOperations: SystemOperations = new SystemOperationsStub();
|
||||
|
||||
public async runCode(): Promise<void> {
|
||||
const environment = new RuntimeEnvironmentStub()
|
||||
.withOs(this.os);
|
||||
const runner = new CodeRunner(this.systemOperations, environment);
|
||||
await runner.runCode(this.code, this.folderName, this.fileExtension);
|
||||
const runner = new TemporaryFileCodeRunner(this.systemOperations);
|
||||
await runner.runCode(this.code, this.folderName, this.fileExtension, this.os);
|
||||
}
|
||||
|
||||
public withSystemOperations(
|
||||
systemOperations: ISystemOperations,
|
||||
systemOperations: SystemOperations,
|
||||
): this {
|
||||
this.systemOperations = systemOperations;
|
||||
return this;
|
||||
@@ -250,22 +242,22 @@ class TestContext {
|
||||
return this.withSystemOperations(stub);
|
||||
}
|
||||
|
||||
public withOs(os: OperatingSystem | undefined) {
|
||||
public withOs(os: OperatingSystem): this {
|
||||
this.os = os;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withFolderName(folderName: string) {
|
||||
public withFolderName(folderName: string): this {
|
||||
this.folderName = folderName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withCode(code: string) {
|
||||
public withCode(code: string): this {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withExtension(fileExtension: string) {
|
||||
public withExtension(fileExtension: string): this {
|
||||
this.fileExtension = fileExtension;
|
||||
return this;
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest';
|
||||
import { validateWindowVariables } from '@/infrastructure/WindowVariables/WindowVariablesValidator';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
|
||||
import { getAbsentObjectTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { WindowVariablesStub } from '@tests/unit/shared/Stubs/WindowVariablesStub';
|
||||
import { CodeRunnerStub } from '@tests/unit/shared/Stubs/CodeRunnerStub';
|
||||
|
||||
describe('WindowVariablesValidator', () => {
|
||||
describe('validateWindowVariables', () => {
|
||||
@@ -92,51 +92,35 @@ describe('WindowVariablesValidator', () => {
|
||||
});
|
||||
|
||||
describe('`isDesktop` property', () => {
|
||||
it('throws an error when only isDesktop is provided and it is true without a system object', () => {
|
||||
it('does not throw when true with valid services', () => {
|
||||
// arrange
|
||||
const systemObject = undefined;
|
||||
const expectedError = getExpectedError(
|
||||
{
|
||||
name: 'system',
|
||||
object: systemObject,
|
||||
},
|
||||
);
|
||||
const validCodeRunner = new CodeRunnerStub();
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsDesktop(true)
|
||||
.withSystem(systemObject);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
|
||||
it('does not throw when isDesktop is true with a valid system object', () => {
|
||||
// arrange
|
||||
const validSystem = new SystemOperationsStub();
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsDesktop(true)
|
||||
.withSystem(validSystem);
|
||||
.withCodeRunner(validCodeRunner);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
});
|
||||
|
||||
it('does not throw when isDesktop is false without a system object', () => {
|
||||
// arrange
|
||||
const absentSystem = undefined;
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsDesktop(false)
|
||||
.withSystem(absentSystem);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
describe('does not throw when false without services', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const absentCodeRunner = absentValue;
|
||||
const input = new WindowVariablesStub()
|
||||
.withIsDesktop(false)
|
||||
.withCodeRunner(absentCodeRunner);
|
||||
// act
|
||||
const act = () => validateWindowVariables(input);
|
||||
// assert
|
||||
expect(act).to.not.throw();
|
||||
}, { excludeNull: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('`system` property', () => {
|
||||
expectObjectOnDesktop('system');
|
||||
describe('`codeRunner` property', () => {
|
||||
expectObjectOnDesktop('codeRunner');
|
||||
});
|
||||
|
||||
describe('`log` property', () => {
|
||||
@@ -158,6 +142,7 @@ function expectObjectOnDesktop<T>(key: keyof WindowVariables) {
|
||||
describe('validates object type on desktop', () => {
|
||||
itEachInvalidObjectValue((invalidObjectValue) => {
|
||||
// arrange
|
||||
const isOnDesktop = true;
|
||||
const invalidObject = invalidObjectValue as T;
|
||||
const expectedError = getExpectedError({
|
||||
name: key,
|
||||
@@ -165,7 +150,7 @@ function expectObjectOnDesktop<T>(key: keyof WindowVariables) {
|
||||
});
|
||||
const input: WindowVariables = {
|
||||
...new WindowVariablesStub(),
|
||||
isDesktop: true,
|
||||
isDesktop: isOnDesktop,
|
||||
[key]: invalidObject,
|
||||
};
|
||||
// act
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getWindowInjectedSystemOperations } from '@/infrastructure/SystemOperations/WindowInjectedSystemOperations';
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
|
||||
|
||||
describe('WindowInjectedSystemOperations', () => {
|
||||
describe('getWindowInjectedSystemOperations', () => {
|
||||
describe('throws if window is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing window';
|
||||
const window: WindowVariables = absentValue as never;
|
||||
// act
|
||||
const act = () => getWindowInjectedSystemOperations(window);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
}, { excludeUndefined: true });
|
||||
});
|
||||
describe('throw if system is absent', () => {
|
||||
itEachAbsentObjectValue((absentValue) => {
|
||||
// arrange
|
||||
const expectedError = 'missing system';
|
||||
const absentSystem = absentValue;
|
||||
const window: Partial<WindowVariables> = {
|
||||
system: absentSystem as never,
|
||||
};
|
||||
// act
|
||||
const act = () => getWindowInjectedSystemOperations(window);
|
||||
// assert
|
||||
expect(act).to.throw(expectedError);
|
||||
});
|
||||
});
|
||||
it('returns from window', () => {
|
||||
// arrange
|
||||
const expectedValue = new SystemOperationsStub();
|
||||
const window: Partial<WindowVariables> = {
|
||||
system: expectedValue,
|
||||
};
|
||||
// act
|
||||
const actualValue = getWindowInjectedSystemOperations(window);
|
||||
// assert
|
||||
expect(actualValue).to.equal(expectedValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user