mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): authorize reconciliation reviews
This commit is contained in:
@@ -315,6 +315,11 @@
|
||||
"require": "./dist/security/securityAuditQuery.js",
|
||||
"default": "./dist/security/securityAuditQuery.js"
|
||||
},
|
||||
"./authentication-read": {
|
||||
"types": "./dist/security/authenticationRead.d.ts",
|
||||
"require": "./dist/security/authenticationRead.js",
|
||||
"default": "./dist/security/authenticationRead.js"
|
||||
},
|
||||
"./security-audit-retention": {
|
||||
"types": "./dist/security/securityAuditRetention.d.ts",
|
||||
"require": "./dist/security/securityAuditRetention.js",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Security owns the read-only credential projection used by stopped-state commands.
|
||||
import type { ApiCredentialRepository } from '@qinglong/runtime-core/api-credential';
|
||||
import type { LocalOwnerPepperRepository } from '@qinglong/runtime-core/local-owner-pepper';
|
||||
|
||||
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
|
||||
import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository';
|
||||
import {
|
||||
assertLocalSqliteOptions,
|
||||
assertLocalSqlitePathBoundary,
|
||||
openLocalSqliteClient,
|
||||
type LocalSqliteDatabaseOptions,
|
||||
type LocalSqliteProfile,
|
||||
} from '../storage/config';
|
||||
import { LocalSqliteApiCredentialRepository } from './apiCredentialRepository';
|
||||
import {
|
||||
auditLocalSqliteReadiness,
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
|
||||
export interface LocalSqliteAuthenticationReadDatabase {
|
||||
readonly profile: LocalSqliteProfile;
|
||||
readonly readiness: LocalSqliteReadinessEvidence;
|
||||
readonly apiCredentials: ApiCredentialRepository;
|
||||
readonly ownerPepper: Pick<LocalOwnerPepperRepository, 'resolveKey'>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens only the repositories needed to authenticate a local User. The SQLite
|
||||
* descriptor is read-only, so stopped-state review cannot change journals,
|
||||
* schema, credential rows, or any other target state.
|
||||
*/
|
||||
export async function openLocalSqliteAuthenticationReadDatabase(
|
||||
options: LocalSqliteDatabaseOptions,
|
||||
): Promise<LocalSqliteAuthenticationReadDatabase> {
|
||||
assertLocalSqliteOptions(options);
|
||||
assertLocalSqlitePathBoundary(options.databasePath, false);
|
||||
const client = openLocalSqliteClient(options, true);
|
||||
try {
|
||||
const readiness = await auditLocalSqliteReadiness(client);
|
||||
const authority = new LocalSqliteOperationAuthority(client);
|
||||
let closePromise: Promise<void> | undefined;
|
||||
return Object.freeze({
|
||||
profile: options.profile,
|
||||
readiness,
|
||||
apiCredentials: new LocalSqliteApiCredentialRepository(authority),
|
||||
ownerPepper: new LocalSqliteOwnerPepperRepository(authority),
|
||||
close() {
|
||||
if (closePromise) return closePromise;
|
||||
closePromise = authority.close();
|
||||
return closePromise;
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (client.isOpen) client.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
openLocalSqliteAuthenticationReadDatabase,
|
||||
} = require('../dist/security/authenticationRead.js');
|
||||
const { migrateLocalSqlitePath } = require('../dist/migration/migration.js');
|
||||
|
||||
test('authentication projection opens the target read-only without journal or file drift', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-auth-read-'));
|
||||
fs.chmodSync(root, 0o700);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const databasePath = path.join(root, 'qinglong3.sqlite');
|
||||
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
|
||||
fs.chmodSync(databasePath, 0o600);
|
||||
const bytes = fs.readFileSync(databasePath);
|
||||
const before = fs.statSync(databasePath, { bigint: true });
|
||||
const database = await openLocalSqliteAuthenticationReadDatabase({
|
||||
databasePath,
|
||||
profile: 'edge',
|
||||
});
|
||||
try {
|
||||
assert.equal(database.profile, 'edge');
|
||||
assert.equal(database.readiness.contractName, 'local-control-core');
|
||||
assert.equal(database.readiness.contractVersion, 50);
|
||||
assert.equal(await database.apiCredentials.resolve('absent'), null);
|
||||
assert.equal(await database.ownerPepper.resolveKey('absent'), null);
|
||||
} finally {
|
||||
await database.close();
|
||||
}
|
||||
const after = fs.statSync(databasePath, { bigint: true });
|
||||
assert.equal(fs.readFileSync(databasePath).equals(bytes), true);
|
||||
assert.equal(after.size, before.size);
|
||||
assert.equal(after.mtimeNs, before.mtimeNs);
|
||||
assert.equal(after.ctimeNs, before.ctimeNs);
|
||||
assert.deepEqual(fs.readdirSync(root).sort(), ['qinglong3.sqlite']);
|
||||
});
|
||||
Reference in New Issue
Block a user