mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Link Existing's Browse did nothing: GET /api/filesystem/browse answered 403
"No filesystem browse roots are available".
Two rules were fighting. /root is a default blocked tree in the attachment
guard, and Codeman running as root — containers, plenty of servers — makes
homedir() exactly /root, so the picker's own allowlisted Home root was blocked;
the other candidates live under it or do not exist. The root list came out
empty and there was nothing the user could open.
The blocked trees exist to keep ~/.ssh and friends out of reach, not to seal off
the user's own home. Only trees that would swallow a configured root whole are
dropped now: /root goes when Home is it (or sits inside it), /etc holds no
configured root and is untouched. Secrets stay protected — isSensitivePath
independently matches .ssh/, .env and credentials* at any depth, and it is what
the directory probe asks about.
⚠️ Navigation must reuse the same narrowed list the roots were chosen with.
Handing the raw trees downstream admits a root and then refuses every path
inside it, which reads as a picker that opens and does nothing.
54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
/**
|
|
* @fileoverview The picker must offer a root when Codeman runs as root.
|
|
*
|
|
* `/root` is a DEFAULT blocked tree in the attachment guard, and Codeman running
|
|
* as root — containers, plenty of servers — makes `homedir()` exactly `/root`.
|
|
* The picker's own allowlisted Home root was therefore blocked by the guard,
|
|
* every other candidate lives under it or does not exist, and the endpoint
|
|
* answered 403 "No filesystem browse roots are available" with nothing the user
|
|
* could open. The fix drops only the trees that would swallow a configured root
|
|
* whole; `isSensitivePath` still guards what is inside.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import { readFileSync } from 'node:fs';
|
|
import { isBlockedAttachmentPath, isUnderTree } from '../src/config/attachment-guard.js';
|
|
|
|
const TREES = ['/root', '/etc'];
|
|
|
|
/** Mirror of pickerBlockedTrees in file-routes.ts. */
|
|
const narrow = (trees: readonly string[], roots: readonly string[]) =>
|
|
roots.length === 0 ? trees : trees.filter((t) => !roots.some((r) => isUnderTree(r, t)));
|
|
|
|
describe('file picker roots when the server runs as root', () => {
|
|
it('drops the tree that would swallow the configured Home root', () => {
|
|
expect(narrow(TREES, ['/root'])).toEqual(['/etc']);
|
|
});
|
|
|
|
it('keeps trees that hold no configured root', () => {
|
|
expect(narrow(TREES, ['/home/alice'])).toEqual(['/root', '/etc']);
|
|
expect(narrow(TREES, [])).toEqual(['/root', '/etc']);
|
|
});
|
|
|
|
it('also frees a root nested under the blocked tree', () => {
|
|
// ~/codeman-cases is /root/codeman-cases when running as root.
|
|
expect(narrow(TREES, ['/root/codeman-cases'])).toEqual(['/etc']);
|
|
});
|
|
|
|
it('still refuses secrets inside the freed tree', () => {
|
|
const trees = narrow(TREES, ['/root']);
|
|
for (const p of ['/root/.ssh/id_rsa', '/root/.aws/credentials', '/root/app/.env']) {
|
|
expect(isBlockedAttachmentPath(p, trees)).toBe(true);
|
|
}
|
|
// …while ordinary files under it become reachable, which is the point.
|
|
expect(isBlockedAttachmentPath('/root/projects/readme.md', trees)).toBe(false);
|
|
});
|
|
|
|
it('navigation reuses the same narrowed list the roots were chosen with', () => {
|
|
// Handing the raw trees to navigation would admit a root and then refuse
|
|
// every path inside it — a picker that opens and then does nothing.
|
|
const src = readFileSync(new URL('../src/web/routes/file-routes.ts', import.meta.url), 'utf8');
|
|
expect(src.match(/pickerBlockedTrees\(/g)?.length).toBeGreaterThanOrEqual(3);
|
|
expect(src).not.toMatch(/blockedTrees:\s*guard\.blockedTrees/);
|
|
});
|
|
});
|