refactor(quota): secure managed provider credentials (#2160)
- add shared owner-only credential storage for OpenCode Go, Ollama Cloud, and Cursor - validate credentials before atomic writes using 0700 directories and 0600 files - replace provider-specific credential routes with an allowlisted lifecycle API - stop automatically reading Ollama's legacy cookie file - stop reading or modifying Cursor's database during regular quota requests - add explicit one-time Cursor credential import without mutating Cursor storage - persist refreshed Cursor credentials only in OpenChamber-managed storage - add Ollama Cloud and Cursor credential controls to provider settings - preserve OpenCode Go tracking through the shared credential flow - add VS Code credential management and Cursor quota parity - reject authentication redirects, enforce request timeouts, and fail on unparseable usage pages - mask stored secrets in API responses and extend quota security coverage - update quota provider documentation
This commit is contained in:
committed by
GitHub
parent
3b92d97795
commit
b09614fd68
@@ -0,0 +1,43 @@
|
||||
import { deleteQuotaCredential, readQuotaCredential, writeQuotaCredential } from './store.js';
|
||||
|
||||
const clean = (value) => typeof value === 'string' && !/[\r\n]/.test(value) ? value.trim() : '';
|
||||
|
||||
export const normalizers = {
|
||||
'opencode-go': (value) => {
|
||||
const workspaceId = clean(value?.workspaceId);
|
||||
let authCookie = clean(value?.authCookie);
|
||||
if (authCookie.startsWith('auth=')) authCookie = authCookie.slice(5).trim();
|
||||
return workspaceId && authCookie ? { workspaceId, authCookie } : null;
|
||||
},
|
||||
'ollama-cloud': (value) => {
|
||||
const cookie = clean(value?.cookie);
|
||||
return cookie ? { cookie } : null;
|
||||
},
|
||||
cursor: (value) => {
|
||||
const accessToken = clean(value?.accessToken);
|
||||
const refreshToken = clean(value?.refreshToken);
|
||||
return accessToken || refreshToken ? { accessToken, refreshToken } : null;
|
||||
},
|
||||
};
|
||||
|
||||
export const readManagedCredential = (providerId) => {
|
||||
const normalize = normalizers[providerId];
|
||||
return normalize ? readQuotaCredential(providerId, normalize) : null;
|
||||
};
|
||||
|
||||
export const writeManagedCredential = (providerId, value) => {
|
||||
const credential = normalizers[providerId]?.(value);
|
||||
if (!credential) throw new Error('Invalid credential');
|
||||
writeQuotaCredential(providerId, credential);
|
||||
return getManagedCredentialStatus(providerId);
|
||||
};
|
||||
|
||||
export const getManagedCredentialStatus = (providerId) => {
|
||||
const credential = readManagedCredential(providerId);
|
||||
if (!credential) return { configured: false };
|
||||
if (providerId === 'opencode-go') return { configured: true, workspaceId: credential.workspaceId, secretMasked: '••••••••' };
|
||||
if (providerId === 'cursor') return { configured: true, hasRefreshToken: Boolean(credential.refreshToken), secretMasked: '••••••••' };
|
||||
return { configured: true, secretMasked: '••••••••' };
|
||||
};
|
||||
|
||||
export const deleteManagedCredential = (providerId) => deleteQuotaCredential(providerId);
|
||||
@@ -0,0 +1,48 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const MANAGED_QUOTA_PROVIDERS = new Set(['opencode-go', 'ollama-cloud', 'cursor']);
|
||||
|
||||
const credentialsDirectory = () => path.join(
|
||||
process.env.OPENCHAMBER_DATA_DIR
|
||||
? path.resolve(process.env.OPENCHAMBER_DATA_DIR)
|
||||
: path.join(os.homedir(), '.config', 'openchamber'),
|
||||
'quota',
|
||||
);
|
||||
|
||||
const credentialPath = (providerId) => {
|
||||
if (!MANAGED_QUOTA_PROVIDERS.has(providerId)) throw new Error('Unsupported credential provider');
|
||||
return path.join(credentialsDirectory(), `${providerId}.json`);
|
||||
};
|
||||
|
||||
export const readQuotaCredential = (providerId, normalize) => {
|
||||
try {
|
||||
return normalize(JSON.parse(fs.readFileSync(credentialPath(providerId), 'utf8')));
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') console.warn(`Failed to read ${providerId} quota credentials`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const writeQuotaCredential = (providerId, credential) => {
|
||||
const target = credentialPath(providerId);
|
||||
const directory = path.dirname(target);
|
||||
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
||||
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
||||
fs.chmodSync(directory, 0o700);
|
||||
try {
|
||||
fs.writeFileSync(temporary, `${JSON.stringify(credential, null, 2)}\n`, { mode: 0o600 });
|
||||
fs.chmodSync(temporary, 0o600);
|
||||
fs.renameSync(temporary, target);
|
||||
fs.chmodSync(target, 0o600);
|
||||
} finally {
|
||||
try { fs.unlinkSync(temporary); } catch {}
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteQuotaCredential = (providerId) => {
|
||||
try { fs.unlinkSync(credentialPath(providerId)); } catch (error) {
|
||||
if (error?.code !== 'ENOENT') throw error;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { afterAll, describe, expect, it } from 'bun:test';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { deleteQuotaCredential, readQuotaCredential, writeQuotaCredential } from './store.js';
|
||||
|
||||
const previousDataDir = process.env.OPENCHAMBER_DATA_DIR;
|
||||
const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-quota-store-'));
|
||||
process.env.OPENCHAMBER_DATA_DIR = temporaryDirectory;
|
||||
|
||||
describe('quota credential store', () => {
|
||||
it('uses owner-only permissions and rejects arbitrary provider paths', () => {
|
||||
writeQuotaCredential('ollama-cloud', { cookie: 'secret' });
|
||||
expect(fs.statSync(path.join(temporaryDirectory, 'quota')).mode & 0o777).toBe(0o700);
|
||||
expect(fs.statSync(path.join(temporaryDirectory, 'quota', 'ollama-cloud.json')).mode & 0o777).toBe(0o600);
|
||||
expect(readQuotaCredential('ollama-cloud', (value) => value)).toEqual({ cookie: 'secret' });
|
||||
expect(() => writeQuotaCredential('../escape', {})).toThrow('Unsupported credential provider');
|
||||
deleteQuotaCredential('ollama-cloud');
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (previousDataDir === undefined) delete process.env.OPENCHAMBER_DATA_DIR;
|
||||
else process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
|
||||
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
||||
});
|
||||
Reference in New Issue
Block a user