mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-04 23:27:00 -07:00
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { copyExtension } from './extension-manager.js';
|
|
|
|
describe('copyExtension permissions', () => {
|
|
let tempDir: string;
|
|
let sourceDir: string;
|
|
let destDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-permission-test-'));
|
|
sourceDir = path.join(tempDir, 'source');
|
|
destDir = path.join(tempDir, 'dest');
|
|
fs.mkdirSync(sourceDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Ensure we can delete the temp directory by making everything writable again
|
|
const makeWritableSync = (p: string) => {
|
|
try {
|
|
const stats = fs.lstatSync(p);
|
|
fs.chmodSync(p, stats.mode | 0o700);
|
|
if (stats.isDirectory()) {
|
|
fs.readdirSync(p).forEach((child) =>
|
|
makeWritableSync(path.join(p, child)),
|
|
);
|
|
}
|
|
} catch {
|
|
// Ignore errors during cleanup
|
|
}
|
|
};
|
|
|
|
if (fs.existsSync(tempDir)) {
|
|
makeWritableSync(tempDir);
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('should make destination writable even if source is read-only', async () => {
|
|
const fileName = 'test.txt';
|
|
const filePath = path.join(sourceDir, fileName);
|
|
fs.writeFileSync(filePath, 'hello');
|
|
|
|
// Make source read-only: 0o555 for directory, 0o444 for file
|
|
fs.chmodSync(filePath, 0o444);
|
|
fs.chmodSync(sourceDir, 0o555);
|
|
|
|
// Verify source is read-only
|
|
expect(() => fs.writeFileSync(filePath, 'fail')).toThrow();
|
|
|
|
// Perform copy
|
|
await copyExtension(sourceDir, destDir);
|
|
|
|
// Verify destination is writable
|
|
const destFilePath = path.join(destDir, fileName);
|
|
const destFileStats = fs.statSync(destFilePath);
|
|
const destDirStats = fs.statSync(destDir);
|
|
|
|
// Check that owner write bits are set (0o200)
|
|
expect(destFileStats.mode & 0o200).toBe(0o200);
|
|
expect(destDirStats.mode & 0o200).toBe(0o200);
|
|
|
|
// Verify we can actually write to the destination file
|
|
fs.writeFileSync(destFilePath, 'writable');
|
|
expect(fs.readFileSync(destFilePath, 'utf-8')).toBe('writable');
|
|
|
|
// Verify we can delete the destination (which requires write bit on destDir)
|
|
fs.rmSync(destFilePath);
|
|
expect(fs.existsSync(destFilePath)).toBe(false);
|
|
});
|
|
|
|
it('should handle nested directories with restrictive permissions', async () => {
|
|
const subDir = path.join(sourceDir, 'subdir');
|
|
fs.mkdirSync(subDir);
|
|
const fileName = 'nested.txt';
|
|
const filePath = path.join(subDir, fileName);
|
|
fs.writeFileSync(filePath, 'nested content');
|
|
|
|
// Make nested structure read-only
|
|
fs.chmodSync(filePath, 0o444);
|
|
fs.chmodSync(subDir, 0o555);
|
|
fs.chmodSync(sourceDir, 0o555);
|
|
|
|
// Perform copy
|
|
await copyExtension(sourceDir, destDir);
|
|
|
|
// Verify nested destination is writable
|
|
const destSubDir = path.join(destDir, 'subdir');
|
|
const destFilePath = path.join(destSubDir, fileName);
|
|
|
|
expect(fs.statSync(destSubDir).mode & 0o200).toBe(0o200);
|
|
expect(fs.statSync(destFilePath).mode & 0o200).toBe(0o200);
|
|
|
|
// Verify we can delete the whole destination tree
|
|
await fs.promises.rm(destDir, { recursive: true, force: true });
|
|
expect(fs.existsSync(destDir)).toBe(false);
|
|
});
|
|
|
|
it('should not follow symlinks or modify symlink targets', async () => {
|
|
const symlinkTarget = path.join(tempDir, 'external-target');
|
|
fs.writeFileSync(symlinkTarget, 'external content');
|
|
// Target is read-only
|
|
fs.chmodSync(symlinkTarget, 0o444);
|
|
|
|
const symlinkPath = path.join(sourceDir, 'symlink-file');
|
|
fs.symlinkSync(symlinkTarget, symlinkPath);
|
|
|
|
// Perform copy
|
|
await copyExtension(sourceDir, destDir);
|
|
|
|
const destSymlinkPath = path.join(destDir, 'symlink-file');
|
|
const destSymlinkStats = fs.lstatSync(destSymlinkPath);
|
|
|
|
// Verify it is still a symlink in the destination
|
|
expect(destSymlinkStats.isSymbolicLink()).toBe(true);
|
|
|
|
// Verify the target (external to the extension) was NOT modified
|
|
const targetStats = fs.statSync(symlinkTarget);
|
|
// Owner write bit should still NOT be set (0o200)
|
|
expect(targetStats.mode & 0o200).toBe(0o000);
|
|
|
|
// Clean up
|
|
fs.chmodSync(symlinkTarget, 0o644);
|
|
});
|
|
});
|