mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* fix(security): scope cross-source reads to the caller grant; close get_page exact-path leak One shared resolveRequestedScope() routes every source-scoped read op (query, code_callers/callees, search_by_image, code_blast/flow, get_page) through a single fail-closed trust+grant ladder: a remote caller's __all__ collapses to its granted sources (never the whole brain) and an explicit out-of-grant source_id is rejected. get_page's exact-match path now honors a federated grant via getPage(sourceIds[]) in both engines. Legacy bearer tokens carry their stored permissions.source_id grant (bounded, never widened). Also retries getConfig on transient connection loss. Closes #1924, #1371, #1393, #1336, #1603. * fix(ingest): non-string frontmatter no longer aborts lint/sync; embed/hook/catalog papercuts Parser coerces a non-string title to a string and falls back to inference for slug/type (never fabricating a "123" slug), with a lint NON_STRING_FIELD finding surfacing the malformed frontmatter; a defensive guard in content-sanity stops a non-string title from crashing the whole lint/sync run brain-wide. Plus: embed --catch-up no longer arms the overflowed 32-bit budget timer (and surfaces unembeddable chunks); the frontmatter pre-commit hook ships a correct .md/.mdx regex; and the skill catalog parses YAML block-scalar descriptions. Closes #1883, #1658, #1556, #1948, #1946, #1840, #1711. * v0.42.37.0 fix(security,ingest): source-isolation grant enforcement + non-string frontmatter guard + papercuts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: add NON_STRING_FIELD frontmatter validation class to docs for v0.42.37.0 The v0.42.37.0 non-string-frontmatter fix added an eighth validation class (NON_STRING_FIELD / lint code frontmatter-non-string-field). Update the two current-state docs that enumerate the validation classes: - skills/frontmatter-guard/SKILL.md (seven->eight + table row) - docs/integrations/pre-commit.md (seven->eight + table row) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
4.1 KiB
TypeScript
92 lines
4.1 KiB
TypeScript
/**
|
|
* #1393 — get_page exact-match path honors the federated source grant.
|
|
*
|
|
* Pre-fix the exact path used scalar `ctx.sourceId` only:
|
|
* const sourceOpts = ctx.sourceId ? { sourceId: ctx.sourceId } : {};
|
|
* A remote OAuth client with a federated `allowedSources` grant (and no single
|
|
* ctx.sourceId) therefore got an UNSCOPED exact lookup — a cross-source read of
|
|
* any page by slug. The fuzzy path was already scoped (#1436); this closes the
|
|
* exact path by (a) routing it through sourceScopeOpts and (b) teaching
|
|
* engine.getPage to honor a `sourceIds[]` array (both engines).
|
|
*/
|
|
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { resetPgliteState } from './helpers/reset-pglite.ts';
|
|
import { operations, OperationError, type OperationContext } from '../src/core/operations.ts';
|
|
|
|
let engine: PGLiteEngine;
|
|
const get_page = operations.find(o => o.name === 'get_page')!;
|
|
|
|
function ctxOf(overrides: Partial<OperationContext> = {}): OperationContext {
|
|
return {
|
|
engine: engine as any,
|
|
config: {} as any,
|
|
logger: console as any,
|
|
dryRun: false,
|
|
remote: true,
|
|
sourceId: 'default',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
if (engine) await engine.disconnect();
|
|
}, 60_000);
|
|
|
|
beforeEach(async () => {
|
|
await resetPgliteState(engine);
|
|
await engine.executeRaw(`INSERT INTO sources (id, name, local_path) VALUES ('alpha', 'alpha', '/tmp/alpha') ON CONFLICT (id) DO NOTHING`);
|
|
await engine.executeRaw(`INSERT INTO sources (id, name, local_path) VALUES ('beta', 'beta', '/tmp/beta') ON CONFLICT (id) DO NOTHING`);
|
|
// Distinct slugs per source so an exact lookup can leak across the boundary.
|
|
await engine.putPage('secret/beta-doc', {
|
|
type: 'note', title: 'Beta secret', compiled_truth: 'beta-only content', frontmatter: {},
|
|
}, { sourceId: 'beta' });
|
|
await engine.putPage('shared/alpha-doc', {
|
|
type: 'note', title: 'Alpha doc', compiled_truth: 'alpha content', frontmatter: {},
|
|
}, { sourceId: 'alpha' });
|
|
});
|
|
|
|
describe('engine.getPage honors sourceIds[] (federated grant)', () => {
|
|
test('sourceIds[] matching the page returns it', async () => {
|
|
const page = await engine.getPage('secret/beta-doc', { sourceIds: ['alpha', 'beta'] });
|
|
expect(page?.title).toBe('Beta secret');
|
|
});
|
|
|
|
test('sourceIds[] NOT containing the page returns null', async () => {
|
|
const page = await engine.getPage('secret/beta-doc', { sourceIds: ['alpha'] });
|
|
expect(page).toBeNull();
|
|
});
|
|
|
|
test('sourceIds[] takes precedence over scalar sourceId', async () => {
|
|
// scalar says alpha, array says beta-only — array wins, page found.
|
|
const page = await engine.getPage('secret/beta-doc', { sourceId: 'alpha', sourceIds: ['beta'] });
|
|
expect(page?.title).toBe('Beta secret');
|
|
});
|
|
});
|
|
|
|
describe('get_page handler closes the cross-source exact-read leak', () => {
|
|
test('remote client granted only [alpha] CANNOT read a beta-only slug', async () => {
|
|
const ctx = ctxOf({ remote: true, auth: { token: 't', clientId: 'c', scopes: [], allowedSources: ['alpha'] } as any });
|
|
// Pre-fix this returned the beta page (leak). Now it is scoped out → 404.
|
|
await expect(get_page.handler(ctx, { slug: 'secret/beta-doc' })).rejects.toBeInstanceOf(OperationError);
|
|
});
|
|
|
|
test('remote client granted [alpha, beta] CAN read the beta slug', async () => {
|
|
const ctx = ctxOf({ remote: true, auth: { token: 't', clientId: 'c', scopes: [], allowedSources: ['alpha', 'beta'] } as any });
|
|
const page: any = await get_page.handler(ctx, { slug: 'secret/beta-doc' });
|
|
expect(page.title).toBe('Beta secret');
|
|
});
|
|
|
|
test('remote client granted only [alpha] CAN read its own alpha slug', async () => {
|
|
const ctx = ctxOf({ remote: true, auth: { token: 't', clientId: 'c', scopes: [], allowedSources: ['alpha'] } as any });
|
|
const page: any = await get_page.handler(ctx, { slug: 'shared/alpha-doc' });
|
|
expect(page.title).toBe('Alpha doc');
|
|
});
|
|
});
|