mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): cut alpha.1 candidate milestone
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qinglong/cluster-postgres",
|
||||
"version": "3.0.0-alpha.0",
|
||||
"version": "3.0.0-alpha.1",
|
||||
"private": true,
|
||||
"description": "QingLong 3.0 cluster-only PostgreSQL driver and schema package",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -33,6 +33,7 @@ export {
|
||||
} from '../connection/certificateAuthority';
|
||||
export * from '../security/identityAdministrationRepository';
|
||||
export * from '../security/apiCredentialAdministrationRepository';
|
||||
export * from '../security/apiCredentialPepperReferenceRepository';
|
||||
export * from '../security/securityAuditQueryRepository';
|
||||
export * from '../worker-credential/workerCredentialAdministrationRepository';
|
||||
export * from '../automation/automationAdministrationRepository';
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
ApiCredentialPepperReferenceUnavailableError,
|
||||
normalizeApiCredentialPepperReferenceKeyId,
|
||||
normalizeApiCredentialPepperReferenceLimit,
|
||||
type ApiCredentialPepperReferenceInspection,
|
||||
type ApiCredentialPepperReferenceRepository,
|
||||
} from '@qinglong/runtime-core/api-credential-pepper-reference';
|
||||
import type { PostgresPool } from '@qinglong/runtime-core';
|
||||
import { assertApiCredentialId } from '@qinglong/runtime-core/api-credential';
|
||||
|
||||
interface ReferenceRow extends Record<string, unknown> {
|
||||
observedAtMs: unknown;
|
||||
credentialId: unknown;
|
||||
}
|
||||
|
||||
const INSPECT_SQL = `
|
||||
WITH clock AS (
|
||||
SELECT floor(
|
||||
extract(epoch FROM statement_timestamp()) * 1000
|
||||
)::bigint AS observed_at_ms
|
||||
), current_references AS (
|
||||
SELECT credential.credential_id
|
||||
FROM "ql3"."api_credentials" AS credential
|
||||
CROSS JOIN clock
|
||||
WHERE credential.pepper_key_id = $1
|
||||
AND credential.state = 'active'
|
||||
AND credential.expires_at_ms > clock.observed_at_ms
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "ql3"."api_credentials" AS newer
|
||||
WHERE newer.credential_id = credential.credential_id
|
||||
AND newer.version > credential.version
|
||||
)
|
||||
ORDER BY credential.credential_id
|
||||
LIMIT $2
|
||||
)
|
||||
SELECT
|
||||
clock.observed_at_ms AS "observedAtMs",
|
||||
reference.credential_id AS "credentialId"
|
||||
FROM clock
|
||||
LEFT JOIN current_references AS reference ON true
|
||||
ORDER BY reference.credential_id
|
||||
`.trim();
|
||||
|
||||
function safeInteger(value: unknown): number {
|
||||
const parsed = typeof value === 'string' ? Number(value) : value;
|
||||
if (!Number.isSafeInteger(parsed) || (parsed as number) < 0) {
|
||||
throw new ApiCredentialPepperReferenceUnavailableError();
|
||||
}
|
||||
return parsed as number;
|
||||
}
|
||||
|
||||
export class PostgresApiCredentialPepperReferenceRepository
|
||||
implements ApiCredentialPepperReferenceRepository
|
||||
{
|
||||
constructor(private readonly pool: Pick<PostgresPool, 'query'>) {
|
||||
if (!pool || typeof pool.query !== 'function') {
|
||||
throw new TypeError(
|
||||
'PostgreSQL API credential pepper reference pool is invalid',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async inspect(
|
||||
requestedPepperKeyId: string,
|
||||
requestedLimit?: number,
|
||||
): Promise<Readonly<ApiCredentialPepperReferenceInspection>> {
|
||||
const pepperKeyId = normalizeApiCredentialPepperReferenceKeyId(
|
||||
requestedPepperKeyId,
|
||||
);
|
||||
const limit = normalizeApiCredentialPepperReferenceLimit(requestedLimit);
|
||||
try {
|
||||
const result = await this.pool.query<ReferenceRow>(INSPECT_SQL, [
|
||||
pepperKeyId,
|
||||
limit + 1,
|
||||
]);
|
||||
if (result.rows.length < 1) {
|
||||
throw new ApiCredentialPepperReferenceUnavailableError();
|
||||
}
|
||||
const observedAtMs = safeInteger(result.rows[0]!.observedAtMs);
|
||||
const emptyReferenceSet =
|
||||
result.rows.length === 1 && result.rows[0]!.credentialId === null;
|
||||
const allCredentialIds = emptyReferenceSet
|
||||
? []
|
||||
: result.rows.map((row) => {
|
||||
if (typeof row.credentialId !== 'string') {
|
||||
throw new ApiCredentialPepperReferenceUnavailableError();
|
||||
}
|
||||
try {
|
||||
assertApiCredentialId(row.credentialId);
|
||||
} catch {
|
||||
throw new ApiCredentialPepperReferenceUnavailableError();
|
||||
}
|
||||
return row.credentialId;
|
||||
});
|
||||
const credentialIds = allCredentialIds.slice(0, limit);
|
||||
if (
|
||||
result.rows.some(
|
||||
(row) => safeInteger(row.observedAtMs) !== observedAtMs,
|
||||
) ||
|
||||
new Set(credentialIds).size !== credentialIds.length
|
||||
) {
|
||||
throw new ApiCredentialPepperReferenceUnavailableError();
|
||||
}
|
||||
return Object.freeze({
|
||||
pepperKeyId,
|
||||
observedAtMs,
|
||||
credentialIds: Object.freeze(credentialIds),
|
||||
hasMore: allCredentialIds.length > limit,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ApiCredentialPepperReferenceUnavailableError) {
|
||||
throw error;
|
||||
}
|
||||
throw new ApiCredentialPepperReferenceUnavailableError();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
PostgresApiCredentialPepperReferenceRepository,
|
||||
} = require('../dist/security/apiCredentialPepperReferenceRepository.js');
|
||||
|
||||
test('returns bounded current pepper references using database time', async () => {
|
||||
const calls = [];
|
||||
const repository = new PostgresApiCredentialPepperReferenceRepository({
|
||||
async query(sql, parameters) {
|
||||
calls.push({ sql, parameters });
|
||||
return {
|
||||
rows: [
|
||||
{ observedAtMs: '1000', credentialId: 'credential-a' },
|
||||
{ observedAtMs: '1000', credentialId: 'credential-b' },
|
||||
{ observedAtMs: '1000', credentialId: 'credential-c' },
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(await repository.inspect('legacy-v1', 2), {
|
||||
pepperKeyId: 'legacy-v1',
|
||||
observedAtMs: 1000,
|
||||
credentialIds: ['credential-a', 'credential-b'],
|
||||
hasMore: true,
|
||||
});
|
||||
assert.deepEqual(calls[0].parameters, ['legacy-v1', 3]);
|
||||
assert.match(calls[0].sql, /statement_timestamp\(\)/);
|
||||
assert.match(calls[0].sql, /newer\.version > credential\.version/);
|
||||
});
|
||||
|
||||
test('represents an empty reference set without losing database time', async () => {
|
||||
const repository = new PostgresApiCredentialPepperReferenceRepository({
|
||||
async query() {
|
||||
return { rows: [{ observedAtMs: 1000, credentialId: null }] };
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await repository.inspect('retired-v1'), {
|
||||
pepperKeyId: 'retired-v1',
|
||||
observedAtMs: 1000,
|
||||
credentialIds: [],
|
||||
hasMore: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('fails closed on invalid input and malformed database rows', async () => {
|
||||
const repository = new PostgresApiCredentialPepperReferenceRepository({
|
||||
async query() {
|
||||
return { rows: [{ observedAtMs: 'invalid', credentialId: null }] };
|
||||
},
|
||||
});
|
||||
await assert.rejects(() => repository.inspect('legacy-v1'), /unavailable/);
|
||||
await assert.rejects(() => repository.inspect('legacy-v1', 65));
|
||||
await assert.rejects(() => repository.inspect('bad key'));
|
||||
|
||||
for (const credentialId of [42, null, 'bad credential']) {
|
||||
const malformedRepository =
|
||||
new PostgresApiCredentialPepperReferenceRepository({
|
||||
async query() {
|
||||
return {
|
||||
rows: [
|
||||
{ observedAtMs: 1000, credentialId: 'credential-a' },
|
||||
{ observedAtMs: 1000, credentialId },
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
() => malformedRepository.inspect('legacy-v1'),
|
||||
/unavailable/,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user