mirror of https://github.com/garrytan/gstack.git
refactor: checkTabAccess uses options object, add own-only tab policy
Refactors checkTabAccess(tabId, clientId, isWrite) to use an options
object { isWrite?, ownOnly? }. Adds tabPolicy === 'own-only' support
in the server command dispatch — scoped tokens with this policy are
restricted to their own tabs for all commands, not just writes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7cf7f6e76e
commit
001ba59be0
|
|
@ -630,15 +630,17 @@ export class BrowserManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a client can access a tab.
|
* Check if a client can access a tab.
|
||||||
* Read access is always allowed. Write access requires ownership.
|
* If ownOnly or isWrite is true, requires ownership.
|
||||||
* Unowned tabs are root-only for writes.
|
* Otherwise (reads), allow by default.
|
||||||
*/
|
*/
|
||||||
checkTabAccess(tabId: number, clientId: string, isWrite: boolean): boolean {
|
checkTabAccess(tabId: number, clientId: string, options: { isWrite?: boolean; ownOnly?: boolean } = {}): boolean {
|
||||||
if (clientId === 'root') return true;
|
if (clientId === 'root') return true;
|
||||||
if (!isWrite) return true;
|
|
||||||
const owner = this.tabOwnership.get(tabId);
|
const owner = this.tabOwnership.get(tabId);
|
||||||
if (!owner) return false; // unowned = root-only for writes
|
if (options.ownOnly || options.isWrite) {
|
||||||
return owner === clientId;
|
if (!owner) return false;
|
||||||
|
return owner === clientId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Transfer tab ownership to a different client. */
|
/** Transfer tab ownership to a different client. */
|
||||||
|
|
|
||||||
|
|
@ -906,9 +906,9 @@ async function handleCommandInternal(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tab ownership check (for scoped tokens) ──────────────
|
// ─── Tab ownership check (for scoped tokens) ──────────────
|
||||||
if (tokenInfo && tokenInfo.clientId !== 'root' && WRITE_COMMANDS.has(command)) {
|
if (tokenInfo && tokenInfo.clientId !== 'root' && (WRITE_COMMANDS.has(command) || tokenInfo.tabPolicy === 'own-only')) {
|
||||||
const targetTab = tabId ?? browserManager.getActiveTabId();
|
const targetTab = tabId ?? browserManager.getActiveTabId();
|
||||||
if (!browserManager.checkTabAccess(targetTab, tokenInfo.clientId, true)) {
|
if (!browserManager.checkTabAccess(targetTab, tokenInfo.clientId, { isWrite: WRITE_COMMANDS.has(command), ownOnly: tokenInfo.tabPolicy === 'own-only' })) {
|
||||||
return {
|
return {
|
||||||
status: 403, json: true,
|
status: 403, json: true,
|
||||||
result: JSON.stringify({
|
result: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,19 @@ describe('Tab Isolation', () => {
|
||||||
|
|
||||||
describe('checkTabAccess', () => {
|
describe('checkTabAccess', () => {
|
||||||
it('root can always access any tab (read)', () => {
|
it('root can always access any tab (read)', () => {
|
||||||
expect(bm.checkTabAccess(1, 'root', false)).toBe(true);
|
expect(bm.checkTabAccess(1, 'root', { isWrite: false })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('root can always access any tab (write)', () => {
|
it('root can always access any tab (write)', () => {
|
||||||
expect(bm.checkTabAccess(1, 'root', true)).toBe(true);
|
expect(bm.checkTabAccess(1, 'root', { isWrite: true })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('any agent can read an unowned tab', () => {
|
it('any agent can read an unowned tab', () => {
|
||||||
expect(bm.checkTabAccess(1, 'agent-1', false)).toBe(true);
|
expect(bm.checkTabAccess(1, 'agent-1', { isWrite: false })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scoped agent cannot write to unowned tab', () => {
|
it('scoped agent cannot write to unowned tab', () => {
|
||||||
expect(bm.checkTabAccess(1, 'agent-1', true)).toBe(false);
|
expect(bm.checkTabAccess(1, 'agent-1', { isWrite: true })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scoped agent can read another agent tab', () => {
|
it('scoped agent can read another agent tab', () => {
|
||||||
|
|
@ -49,12 +49,12 @@ describe('Tab Isolation', () => {
|
||||||
// with a known owner via the internal state
|
// with a known owner via the internal state
|
||||||
// We'll use transferTab which only checks pages map... let's test checkTabAccess directly
|
// We'll use transferTab which only checks pages map... let's test checkTabAccess directly
|
||||||
// checkTabAccess reads from tabOwnership map, which is empty here
|
// checkTabAccess reads from tabOwnership map, which is empty here
|
||||||
expect(bm.checkTabAccess(1, 'agent-2', false)).toBe(true);
|
expect(bm.checkTabAccess(1, 'agent-2', { isWrite: false })).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scoped agent cannot write to another agent tab', () => {
|
it('scoped agent cannot write to another agent tab', () => {
|
||||||
// With no ownership set, this is an unowned tab -> denied
|
// With no ownership set, this is an unowned tab -> denied
|
||||||
expect(bm.checkTabAccess(1, 'agent-2', true)).toBe(false);
|
expect(bm.checkTabAccess(1, 'agent-2', { isWrite: true })).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue